packages feed

haskell-opentimestamps-client (empty) → 0.4.1.0

raw patch · 9 files changed

+833/−0 lines, 9 filesdep +basedep +base16dep +bytestring

Dependencies added: base, base16, bytestring, containers, cryptonite, directory, filepath, haskell-opentimestamps, haskell-opentimestamps-client, memory, optparse-applicative, temporary, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,17 @@+0.1.0.0, released 2025-09-04+    Initial version (work in progress)++0.2.0.0, released 2025-09-12+    Removed umbrella project (this library is now standalone)++0.2.1.0, released 2025-09-12+    Added release.sh (required for release build)++0.3.0.0, released 2025-09-12+    Updated version in package.yaml++0.4.0.0, released 2025-09-17+    Updated version reminder++0.4.1.0, released 2025-12-02+    Publish to Hackage
+ LICENSE view
@@ -0,0 +1,217 @@+Copyright A.H.M. (Mari) Donkers 2025.++Licensed under either of:++* BSD-3-Clause license (https://opensource.org/licenses/BSD-3-Clause)+* Apache License, version 2.0 (https://opensource.org/licenses/Apache-2.0)++As a user, you may use this code under either license, at your option.+++---------------------------------------------------------------------+BSD 3-Clause License+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Neil Mitchell nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+++---------------------------------------------------------------------+Apache License+Version 2.0, January 2004+<http://www.apache.org/licenses/>++Terms and Conditions for use, reproduction, and distribution+1. Definitions+"License" shall mean the terms and conditions for use, reproduction,+and distribution as defined by Sections 1 through 9 of this document.++"Licensor" shall mean the copyright owner or entity authorized by the+copyright owner that is granting the License.++"Legal Entity" shall mean the union of the acting entity and all+other entities that control, are controlled by, or are under common+control with that entity. For the purposes of this definition,+"control" means (i) the power, direct or indirect, to cause the+direction or management of such entity, whether by contract or+otherwise, or (ii) ownership of fifty percent (50%) or more of the+outstanding shares, or (iii) beneficial ownership of such entity.++"You" (or "Your") shall mean an individual or Legal Entity exercising+permissions granted by this License.++"Source" form shall mean the preferred form for making modifications,+including but not limited to software source code, documentation+source, and configuration files.++"Object" form shall mean any form resulting from mechanical+transformation or translation of a Source form, including but not+limited to compiled object code, generated documentation, and+conversions to other media types.++"Work" shall mean the work of authorship, whether in Source or Object+form, made available under the License, as indicated by a copyright+notice that is included in or attached to the work).++"Derivative Works" shall mean any work, whether in Source or Object+form, that is based on (or derived from) the Work and for which the+editorial revisions, annotations, elaborations, or other+modifications represent, as a whole, an original work of authorship.+For the purposes of this License, Derivative Works shall not include+works that remain separable from, or merely link (or bind by name) to+the interfaces of, the Work and Derivative Works thereof.++"Contribution" shall mean any work of authorship, including the+original version of the Work and any modifications or additions to+that Work or Derivative Works thereof, that is intentionally+submitted to Licensor for inclusion in the Work by the copyright+owner or by an individual or Legal Entity authorized to submit on+behalf of the copyright owner. For the purposes of this definition,+"submitted" means any form of electronic, verbal, or written+communication sent to the Licensor or its representatives, including+but not limited to communication on electronic mailing lists, source+code control systems, and issue tracking systems that are managed by,+or on behalf of, the Licensor for the purpose of discussing and+improving the Work, but excluding communication that is conspicuously+marked or otherwise designated in writing by the copyright owner as+"Not a Contribution."++"Contributor" shall mean Licensor and any individual or Legal Entity+on behalf of whom a Contribution has been received by Licensor and+subsequently incorporated within the Work.++2. Grant of Copyright License+Subject to the terms and conditions of this License, each Contributor+hereby grants to You a perpetual, worldwide, non-exclusive,+no-charge, royalty-free, irrevocable copyright license to reproduce,+prepare Derivative Works of, publicly display, publicly perform,+sublicense, and distribute the Work and such Derivative Works in+Source or Object form.++3. Grant of Patent License+Subject to the terms and conditions of this License, each Contributor+hereby grants to You a perpetual, worldwide, non-exclusive,+no-charge, royalty-free, irrevocable (except as stated in this+section) patent license to make, have made, use, offer to sell, sell,+import, and otherwise transfer the Work, where such license applies+only to those patent claims licensable by such Contributor that are+necessarily infringed by their Contribution(s) alone or by+combination of their Contribution(s) with the Work to which such+Contribution(s) was submitted. If You institute patent litigation+against any entity (including a cross-claim or counterclaim in a+lawsuit) alleging that the Work or a Contribution incorporated within+the Work constitutes direct or contributory patent infringement, then+any patent licenses granted to You under this License for that Work+shall terminate as of the date such litigation is filed.++4. Redistribution+You may reproduce and distribute copies of the Work or Derivative+Works thereof in any medium, with or without modifications, and in+Source or Object form, provided that You meet the following+conditions:++(a) You must give any other recipients of the Work or Derivative+Works a copy of this License; and+(b) You must cause any modified files to carry prominent notices+stating that You changed the files; and+(c) You must retain, in the Source form of any Derivative Works that+You distribute, all copyright, patent, trademark, and attribution+notices from the Source form of the Work, excluding those notices+that do not pertain to any part of the Derivative Works; and+(d) If the Work includes a "NOTICE" text file as part of its+distribution, then any Derivative Works that You distribute must+include a readable copy of the attribution notices contained within+such NOTICE file, excluding those notices that do not pertain to any+part of the Derivative Works, in at least one of the following+places: within a NOTICE text file distributed as part of the+Derivative Works; within the Source form or documentation, if+provided along with the Derivative Works; or, within a display+generated by the Derivative Works, if and wherever such third-party+notices normally appear. The contents of the NOTICE file are for+informational purposes only and do not modify the License. You may+add Your own attribution notices within Derivative Works that You+distribute, alongside or as an addendum to the NOTICE text from the+Work, provided that such additional attribution notices cannot be+construed as modifying the License.+You may add Your own copyright statement to Your modifications and+may provide additional or different license terms and conditions for+use, reproduction, or distribution of Your modifications, or for any+such Derivative Works as a whole, provided Your use, reproduction,+and distribution of the Work otherwise complies with the conditions+stated in this License.++5. Submission of Contributions+Unless You explicitly state otherwise, any Contribution intentionally+submitted for inclusion in the Work by You to the Licensor shall be+under the terms and conditions of this License, without any+additional terms or conditions. Notwithstanding the above, nothing+herein shall supersede or modify the terms of any separate license+agreement you may have executed with Licensor regarding such+Contributions.++6. Trademarks+This License does not grant permission to use the trade names,+trademarks, service marks, or product names of the Licensor, except+as required for reasonable and customary use in describing the origin+of the Work and reproducing the content of the NOTICE file.++7. Disclaimer of Warranty+Unless required by applicable law or agreed to in writing, Licensor+provides the Work (and each Contributor provides its Contributions)+on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,+either express or implied, including, without limitation, any+warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY,+or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for+determining the appropriateness of using or redistributing the Work+and assume any risks associated with Your exercise of permissions+under this License.++8. Limitation of Liability+In no event and under no legal theory, whether in tort (including+negligence), contract, or otherwise, unless required by applicable+law (such as deliberate and grossly negligent acts) or agreed to in+writing, shall any Contributor be liable to You for damages,+including any direct, indirect, special, incidental, or consequential+damages of any character arising as a result of this License or out+of the use or inability to use the Work (including but not limited to+damages for loss of goodwill, work stoppage, computer failure or+malfunction, or any and all other commercial damages or losses), even+if such Contributor has been advised of the possibility of such+damages.++9. Accepting Warranty or Additional Liability+While redistributing the Work or Derivative Works thereof, You may+choose to offer, and charge a fee for, acceptance of support,+warranty, indemnity, or other liability obligations and/or rights+consistent with this License. However, in accepting such obligations,+You may act only on Your own behalf and on Your sole responsibility,+not on behalf of any other Contributor, and only if You agree to+indemnify, defend, and hold each Contributor harmless for any+liability incurred by, or claims asserted against, such Contributor+by reason of your accepting any such warranty or additional+liability.
+ Makefile view
@@ -0,0 +1,70 @@+PROJECT=haskell-opentimestamps-client++.PHONY: help+help: ## print make targets+	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'++package: package.yaml+	hpack++.PHONY: freeze+freeze:  ## Freeze dependencies+	cabal freeze++.PHONY: clean+clean:   ## Clean project+	cabal clean++.PHONY: build+build:  package ## Build project+	cabal build++.PHONY: repl+repl:   package ## Repl with project pre-loaded+	cabal repl $(PROJECT)++.PHONY: format+format:   ## Format Haskell source code+	fourmolu -i `ag -l --haskell`++.PHONY: lint+lint:   ## Lint Haskell source code+	hlint `ag -l --haskell`++.PHONY: ls+ls:     ## get path of executable+	cabal ls-bin $(PROJECT)++.PHONY: run+run:    package ## run the project with cabal+	cabal run++.PHONY: release+release:  ## release build (no debug symbols, etc).+	cabal build --enable-executable-stripping all+	cabal list-bin "$(PROJECT)-exe"++.PHONY: check+check:  ## runs cabal check+	cabal check++.PHONY: sdist+sdist:  ## generate a distribution tarball+	cabal sdist++# HOOGLE="http://localhost:4041" emacs &+.PHONY: edit+edit:   ## Start editor+	emacs &++# https://blog.ssanj.net/posts/2019-10-19-running-hoogle-locally-for-haskell-dev.html+.PHONY: hoogledb+hoogledb: ## Create local Hoogle database+	cabal-hoogle generate+	# stack hoogle -- generate --local++# https://blog.ssanj.net/posts/2019-10-19-running-hoogle-locally-for-haskell-dev.html+.PHONY: hoogle+hoogle: ## Run local Hoogle server+	cabal-hoogle run -- server --local --port 4041+	# stack hoogle -- server --local --port=4041
+ README.md view
@@ -0,0 +1,30 @@+This is a Haskell client for OpenTimestamps, providing a command-line utility to interact with the OpenTimestamps network. It's on Hackage here: [haskell-opentimestamps-client](https://hackage.haskell.org/package/haskell-opentimestamps-client). The client program allows users to:+*   **Info:** Display information about a timestamp file.+*   **Stamp:** Create OpenTimestamps for files.+*   **Prune:** Prune a timestamp file.+*   **Upgrade:** Upgrade remote calendar timestamps to be locally verifiable.+*   **Verify:** Verify existing OpenTimestamps files.++The client uses the `haskell-opentimestamps` library for core OpenTimestamps stuff.++The project has a number of dependencies, including `cryptonite` for cryptographic operations, `http-client` for making requests to calendar servers and `haskoin-code` and `bitcoin-rpc` to make RPC-calls to a locally running 'blocksonly' pruned Bitcoin node, which is required for **Verification**.++Also you will need to have a `libsecp256k1` package (Debian: `libsecp256k1-dev`) installed on your Linux distribution (beware: it needs to be < 0.7.x because of deprecated functions in `0.7.x`).++To build an executable:++```sh+make release+```++Updating version (development reminder):+- `version: #.#.#.#` in `package.yaml`+- run `hpack` to regenerate the `.cabal` file+- Add the version to `CHANGELOG.md`+- Commit the new version.+- `git tag -a #.#.#.#` to add a git tag+- `git push yourorigin #.#.#.#` to push the git tag++**Work In Progress**...++There is also a Haskell library for working with OpenTimestamps. It's on Hackage here: [haskell-opentimestamps](https://hackage.haskell.org/package/haskell-opentimestamps).
+ app/Main.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main (main) where++import qualified OpenTimestampsClient as OTC+import Options.Applicative+  ( Parser+  , command+  , execParser+  , fullDesc+  , help+  , helper+  , info+  , long+  , metavar+  , progDesc+  , short+  , some+  , strArgument+  , subparser+  , switch+  , (<**>)+  )++calendarUrls :: [String]+calendarUrls =+  [ "https://a.pool.opentimestamps.org"+  , "https://b.pool.opentimestamps.org"+  , "https://a.pool.eternitywall.com"+  , "https://ots.btc.catallaxy.com"+  ]++data GlobalOptions where+  GlobalOptions :: {globalVerbose :: Bool} -> GlobalOptions++verboseOption :: Parser Bool+verboseOption =+  switch+    ( long "verbose"+        <> short 'v'+        <> help "Enable verbose output"+    )++globalOptionsParser :: Parser GlobalOptions+globalOptionsParser =+  GlobalOptions+    <$> verboseOption++-- TODO Add GitExtract (?) to Extract timestamp for a single file from a timestamp git commit++data Command where+  Info :: FilePath -> Command+  Prune :: FilePath -> Command+  Stamp :: [FilePath] -> Command+  Upgrade :: FilePath -> Command+  Verify :: FilePath -> Command++infoParser :: Parser Command+infoParser =+  Info+    <$> strArgument+      ( metavar "FILE"+          <> help "Timestamp file (.ots)"+      )++pruneParser :: Parser Command+pruneParser =+  Prune+    <$> strArgument+      ( metavar "FILE"+          <> help "[TODO] Timestamp file (.ots)"+      )++stampParser :: Parser Command+stampParser =+  Stamp+    <$> some+      ( strArgument+          ( metavar "FILE"+              <> help "File(s) to timestamp"+          )+      )++upgradeParser :: Parser Command+upgradeParser =+  Upgrade+    <$> strArgument+      ( metavar "FILE"+          <> help "[TODO] Timestamp file (.ots)"+      )++verifyParser :: Parser Command+verifyParser =+  Verify+    <$> strArgument+      ( metavar "FILE"+          <> help "Timestamp file (.ots)"+      )++commandParser :: Parser Command+commandParser =+  subparser+    ( command+        "info"+        (info infoParser (progDesc "Show information on a timestamp"))+        <> command+          "prune"+          (info pruneParser (progDesc "Prune timestamp"))+        <> command+          "stamp"+          (info stampParser (progDesc "Timestamp file(s)"))+        <> command+          "upgrade"+          ( info+              upgradeParser+              (progDesc "Upgrade remote calendar timestamps to be locally verifiable")+          )+        <> command+          "verify"+          (info verifyParser (progDesc "Verify a timestamp"))+    )++data AppParser where+  AppParser :: GlobalOptions -> Command -> AppParser++appParser :: Parser AppParser+appParser = AppParser <$> globalOptionsParser <*> commandParser++-- | Main entry point of the application.+main :: IO ()+main = do+  AppParser gOpts cmd <-+    execParser (info (appParser <**> helper) (fullDesc <> progDesc "OpenTimestamps client"))+  run gOpts cmd+  where+    run :: GlobalOptions -> Command -> IO ()+    run _gOpts (Info path) = do+      -- putStrLn $ "Getting info for: " ++ path+      OTC.info path+    run gOpts (Prune path) = do+      OTC.prune (globalVerbose gOpts) path [""] [""] -- TODO verify- and discard args+    run gOpts (Stamp paths) = do+      OTC.stamp (globalVerbose gOpts) paths calendarUrls+    run gOpts (Upgrade path) = do+      OTC.upgrade (globalVerbose gOpts) path+    run gOpts (Verify path) = do+      OTC.verify (globalVerbose gOpts) path
+ cabal.project view
@@ -0,0 +1,21 @@+packages: .+          +source-repository-package+  type: git+  location: https://github.com/bitnomial/bitcoin-compact-filters+  -- branch: master+  tag: 7ae4d9bec8a5275db579e34e3cadb86c0f0461e9++source-repository-package+  type: git+  location: https://github.com/bitnomial/bitcoind-rpc+  -- branch: master+  tag: 16d664e05c232800aa6bc4f63c372c6f3e0e5d39+  subdir: bitcoind-rpc++source-repository-package+  type: git+  location: https://codeberg.org/photonsphere/haskell-opentimestamps.git+  --location: /absolutepathonyourcomputerhere/haskell-opentimestamps+  branch: master+  --tag: 0.1.0.0
+ haskell-opentimestamps-client.cabal view
@@ -0,0 +1,66 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.38.2.+--+-- see: https://github.com/sol/hpack++name:           haskell-opentimestamps-client+version:        0.4.1.0+synopsis:       Haskell implementation of a basic OpenTimestamps client+description:    Please see the README on Codeberg at <https://codeberg.org/photonsphere/haskell-opentimestamps-client#readme>+category:       Bitcoin, Finance, Network, Cryptography+author:         A.H.M. (Mari) Donkers+maintainer:     a.h.m.donkers@photonsphere.org+copyright:      2025 A.H.M. (Mari) Donkers+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md+    cabal.project+    package.yaml+    Makefile++source-repository head+  type: git+  location: https://codeberg.org/photonsphere/haskell-opentimestamps-client++library+  exposed-modules:+      OpenTimestampsClient+  other-modules:+      Paths_haskell_opentimestamps_client+  hs-source-dirs:+      src+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base >=4.7 && <5+    , base16 >=0.1 && <2.0+    , bytestring >=0.10 && <0.13+    , containers >=0.5 && <0.8+    , cryptonite >=0.25 && <0.35+    , directory >=1.2 && <1.4+    , filepath >=1.3 && <1.5+    , haskell-opentimestamps >=0.5.5.0 && <1.0+    , memory >=0.14 && <0.19+    , temporary >=1.2 && <1.4+    , text >=1.2 && <2.2+    , time >=1.4 && <1.13+  default-language: Haskell2010++executable haskell-opentimestamps-client-exe+  main-is: Main.hs+  other-modules:+      Paths_haskell_opentimestamps_client+  hs-source-dirs:+      app+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , bytestring >=0.10 && <0.13+    , cryptonite >=0.25 && <0.35+    , haskell-opentimestamps-client+    , memory >=0.14 && <0.19+    , optparse-applicative >=0.14 && <0.19+  default-language: Haskell2010
+ package.yaml view
@@ -0,0 +1,63 @@+name:                haskell-opentimestamps-client+version:             0.4.1.0+license:             BSD3+#x-license:           BSD-3-Clause OR Apache-2.0+author:              "A.H.M. (Mari) Donkers"+maintainer:          "a.h.m.donkers@photonsphere.org"+copyright:           "2025 A.H.M. (Mari) Donkers"+git:                 "https://codeberg.org/photonsphere/haskell-opentimestamps-client"+category:            Bitcoin, Finance, Network, Cryptography+synopsis:            Haskell implementation of a basic OpenTimestamps client++extra-source-files:+- README.md+- CHANGELOG.md+- cabal.project+- package.yaml+- Makefile++description:         Please see the README on Codeberg at <https://codeberg.org/photonsphere/haskell-opentimestamps-client#readme>++ghc-options:+- -Wall+- -Wcompat+- -Widentities+- -Wincomplete-record-updates+- -Wincomplete-uni-patterns+- -Wmissing-export-lists+- -Wmissing-home-modules+- -Wpartial-fields+- -Wredundant-constraints++library:+  source-dirs: src+  dependencies:+    - base >= 4.7 && < 5+    - bytestring >= 0.10 && < 0.13+    - cryptonite >= 0.25 && < 0.35+    - directory >= 1.2 && < 1.4+    - filepath >= 1.3 && < 1.5+    - haskell-opentimestamps >= 0.5.5.0 && < 1.0+    - memory >= 0.14 && < 0.19+    - temporary >= 1.2 && < 1.4+    - text >= 1.2 && < 2.2+    - time >= 1.4 && < 1.13+    - base16 >= 0.1 && < 2.0+    - containers >= 0.5 && < 0.8++executables:+  haskell-opentimestamps-client-exe:+    main:                Main.hs+    source-dirs:         app+    ghc-options:+    - -threaded+    - -rtsopts+    - -with-rtsopts=-N+    dependencies:+    - base >= 4.7 && < 5+    - bytestring >= 0.10 && < 0.13+    - cryptonite >= 0.25 && < 0.35+    - haskell-opentimestamps-client+    - memory >= 0.14 && < 0.19+    - optparse-applicative >= 0.14 && < 0.19+
+ src/OpenTimestampsClient.hs view
@@ -0,0 +1,200 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}++module OpenTimestampsClient+  ( info+  , prune+  , stamp+  , upgrade+  , verify+  ) where++import Control.Monad (unless, when)+import Crypto.Hash (SHA256 (SHA256), hashWith)+import qualified Data.ByteArray as BA+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy.Char8 as BSL+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import Data.Time.Format (defaultTimeLocale, formatTime)+import Data.Time.LocalTime (TimeZone (..), utcToZonedTime)+import qualified OpenTimestamps as OT+import qualified OpenTimestamps.DetachedTimestampFile as DTSF+import OpenTimestamps.Timestamp (printHex, printTimestamp, timestampMsg)+import qualified OpenTimestamps.Verify as Verify+import System.Directory (doesFileExist, renameFile)+import System.FilePath (addExtension, stripExtension, takeDirectory)+import System.IO (hClose, hPutStrLn, stderr)+import System.IO.Temp (withTempFile)++data RPCCredentials where+  RPCCredentials ::+    {rpcUser :: T.Text, rpcPassword :: T.Text} ->+    RPCCredentials+  deriving (Show, Eq)++readCookieFile :: FilePath -> IO (Maybe RPCCredentials)+readCookieFile path = do+  exists <- doesFileExist path+  if exists+    then do+      content <- TIO.readFile path+      case T.splitOn ":" content of+        [user, pass] -> return $ Just $ RPCCredentials user pass+        _ -> return Nothing+    else do+      hPutStrLn stderr $ "Cookie file not found at: " ++ path+      return Nothing++info :: FilePath -> IO ()+info path =+  processStream =<< BSL.readFile path+  where+    processStream otsFileContent = case DTSF.deserialize otsFileContent of+      Left err ->+        putStrLn $ "Error deserializing: " ++ show err+      Right dtfs -> do+        putStrLn $+          "File sha256 hash: "+            ++ printHex+              ( timestampMsg+                  (DTSF.timestamp dtfs)+              )+        putStrLn "Timestamp:"+        putStrLn $ printTimestamp 0 (DTSF.timestamp dtfs)++prune :: Bool -> FilePath -> [String] -> [String] -> IO ()+prune verbose path verifyArgs discardArgs = do+  when verbose $ putStrLn $ "Reading timestamp file: " ++ path+  otsFileContent <- BSL.readFile path+  when verbose $ putStrLn $ "Pruning timestamp: " ++ path+  result <- OT.prune verifyArgs discardArgs otsFileContent+  case result of+    Left err -> do+      hPutStrLn stderr $ "Error pruning timestamp: " ++ err+    Right newOtsContent -> do+      let dir = takeDirectory path+      let tmpFileName = path ++ ".tmp"+      tmpExists <- doesFileExist tmpFileName+      when tmpExists $+        fail $+          "Intermediate temporary file " ++ tmpFileName ++ " already exists"+      let bakFileName = path ++ ".bak"+      bakExists <- doesFileExist bakFileName+      when bakExists $+        fail $+          "Backup file " ++ bakFileName ++ " already exists"++      withTempFile+        dir+        tmpFileName+        ( \tempPath tempHandle -> do+            BSL.hPutStr tempHandle newOtsContent+            hClose tempHandle+            renameFile path bakFileName+            renameFile tempPath path+            putStrLn $ "Timestamp pruned and saved to " ++ path+        )++stamp :: Bool -> [FilePath] -> [String] -> IO ()+stamp verbose paths calendarUrls = do+  mapM_ stampOne paths+  where+    stampOne path = do+      let newOtsPath = addExtension path ".ots"+      exists <- doesFileExist newOtsPath+      when exists $+        fail $+          "Failed to create timestamp '" ++ newOtsPath ++ "' (file already exists)."+      when verbose $ putStrLn $ "Reading content of: " ++ path+      content <- BS.readFile path+      -- TODO Use OpenTimestamps library function (t.b.d.) with type.+      let digest = BA.convert (hashWith SHA256 content)+      when verbose $ putStrLn $ "Stamping: " ++ path+      sr <- OT.stamp calendarUrls digest+      -- TODO Nicer error formatting.+      let errors = OT.srErrors sr+      if verbose+        then mapM_ print errors+        else+          unless (null errors) $+            hPutStrLn stderr "There were calender servers with errors (use global option -v for details)"+      case OT.srTimestampFile sr of+        Nothing -> hPutStrLn stderr "No timestamps could be retrieved"+        Just dtsf -> do+          let dtfsNew = OT.serialize dtsf+          BSL.writeFile newOtsPath dtfsNew+          putStrLn $ "Timestamp saved to " ++ newOtsPath++upgrade :: Bool -> FilePath -> IO ()+upgrade verbose path = do+  when verbose $ putStrLn $ "Reading timestamp file: " ++ path+  otsFileContent <- BSL.readFile path+  when verbose $ putStrLn $ "Upgrading timestamp: " ++ path+  -- TODO The `[]` is for calendarUrls (what to do with them?)++  result <- OT.upgrade [] otsFileContent+  case result of+    Left err -> do+      hPutStrLn stderr $ "Error upgrading timestamp: " ++ err+    Right newOtsContent -> do+      let dir = takeDirectory path+      let tmpFileName = path ++ ".tmp"+      tmpExists <- doesFileExist tmpFileName+      when tmpExists $ fail $ "Intermediate temporary file " ++ tmpFileName ++ " already exists"+      let bakFileName = path ++ ".bak"+      bakExists <- doesFileExist bakFileName+      when bakExists $ fail $ "Backup file " ++ bakFileName ++ " already exists"++      withTempFile+        dir+        tmpFileName+        ( \tempPath tempHandle -> do+            BSL.hPutStr tempHandle newOtsContent+            hClose tempHandle+            renameFile path bakFileName+            renameFile tempPath path+            putStrLn $ "Timestamp upgraded and saved to " ++ path+        )++verify :: Bool -> FilePath -> IO ()+verify verbose path = do+  let cookieFilePath = "/home/mdo/.bitcoin/.cookie"+  mrpcc <- readCookieFile cookieFilePath+  let bitcoinConfig = case mrpcc of+        Nothing ->+          Verify.BitcoinConfig+            { Verify.bitcoinHost = "localhost"+            , Verify.bitcoinPort = 8332+            , Verify.bitcoinUser = "bitcoin"+            , Verify.bitcoinPass = "bitcoinpass"+            }+        Just rpcc ->+          Verify.BitcoinConfig+            { Verify.bitcoinHost = "localhost"+            , Verify.bitcoinPort = 8332+            , Verify.bitcoinUser = T.unpack $ rpcUser rpcc+            , Verify.bitcoinPass = T.unpack $ rpcPassword rpcc+            }++  print $ "Cookiefile path: " ++ cookieFilePath+  -- print $ "User: " ++ Verify.bitcoinUser bitcoinConfig+  -- print $ "Password: " ++ Verify.bitcoinPass bitcoinConfig+  case stripExtension ".ots" path of+    Nothing -> hPutStrLn stderr $ "Not a timestamp? " ++ path+    Just srcPath -> do+      content <- BS.readFile srcPath+      let digest = BA.convert (hashWith SHA256 content)+      when verbose $ putStrLn $ "Reading timestamp file: " ++ path+      otsFileContent <- BSL.readFile path+      when verbose $ putStrLn $ "Verifying timestamp: " ++ path+      -- TODO The `[]` is for calendarUrls (what to do with them?)++      result <- OT.verify bitcoinConfig [] otsFileContent digest+      case result of+        Right (blockHeight, utcTime) -> do+          let zonedTime = utcToZonedTime (TimeZone (-(1 * 60 * 4)) False "CEST") utcTime -- TODO Assuming CEST is UTC+2, so -2 hours from UTC+          let formattedTime = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S %Z" zonedTime+          putStrLn $+            "Success! Bitcoin block " ++ show blockHeight ++ " attests existence as of " ++ formattedTime+        Left err -> putStrLn $ "Failed " ++ show err