Initial commit
This commit is contained in:
commit
a100087646
14 changed files with 1051 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.stack-work/
|
||||
*~
|
20
CHANGELOG.md
Normal file
20
CHANGELOG.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- CHANGELOG.md
|
||||
- README.md
|
||||
- List node addresses
|
||||
- Query an address balance
|
||||
- List transactions for an address, displaying decoded memos
|
||||
- Copy address to clipboard
|
||||
- Create new Unified Addresses
|
||||
- Sending transactions
|
||||
|
23
LICENSE
Normal file
23
LICENSE
Normal file
|
@ -0,0 +1,23 @@
|
|||
[The MIT License (MIT)][]
|
||||
|
||||
Copyright (c) 2022 Rene Vergara
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
[The MIT License (MIT)]: https://opensource.org/licenses/MIT
|
63
README.md
Normal file
63
README.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Zenith
|
||||
|
||||
```
|
||||
______ _ _ _
|
||||
|___ / (_) | | |
|
||||
/ / ___ _ __ _| |_| |__
|
||||
/ / / _ \ '_ \| | __| '_ \
|
||||
/ /_| __/ | | | | |_| | | |
|
||||
/_____\___|_| |_|_|\__|_| |_|
|
||||
Zcash Full Node CLI
|
||||
```
|
||||
|
||||
Zenith is a command-line interface for the Zcash Full Node (`zcashd`). It has the following features:
|
||||
|
||||
- Listing transparent and shielded addresses and balances known to the node, including viewing-only.
|
||||
- Listing transactions for specific addresses, decoding memos for easy reading.
|
||||
- Copying addresses to the clipboard.
|
||||
- Creating new Unified Addresses.
|
||||
- Sending transactions with shielded memo support.
|
||||
|
||||
Note: Zenith depends on a patched version of the `haskoin-core` Haskell package included in this repo. A pull request to the maintainers of `haskoin-core` has been submitted, if/when it is merged, Zenith will be updated to use the standard package.
|
||||
|
||||
## Installation
|
||||
|
||||
- Clone the repository.
|
||||
- Install dependencies:
|
||||
- [Stack](https://docs.haskellstack.org/en/stable/README/#how-to-install)
|
||||
- [Zcash Full Node v.5.0.0](https://zcash.readthedocs.io/en/latest/rtd_pages/zcashd.html#install)
|
||||
- `xclip`
|
||||
- `libsecp256k1-dev`
|
||||
- `libxss-dev`
|
||||
- Install using `stack`:
|
||||
|
||||
```
|
||||
stack install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
- Copy the sample `zenith.cfg` file to a location of your choice and update the values of the user and password for the `zcashd` node. These values can be found in the `zcash.conf` file for the Zcash node.
|
||||
|
||||
## Usage
|
||||
|
||||
From the location where the configured `zenith.cfg` file is placed, use `zenith` to start.
|
||||
|
||||
Zenith will attempt to connect to the node and check compatibility. Connections to `zcashd` versions less than 5.0.0 will fail.
|
||||
|
||||
### Available commands
|
||||
|
||||
- `?`: Lists available commands.
|
||||
- `list`: Lists all transparent and shielded addresses and their balance.
|
||||
- Notes about balances:
|
||||
- Addresses from an imported viewing key will list a balance but it may be inaccurate, as viewing keys cannot see ZEC spent out of that address.
|
||||
- Balances for Unified Addresses *belonging to the same account* are shared. Zenith will list the full account balances for each of the UAs in the account.
|
||||
- `txs <id>`: Lists all transactions belonging to the address corresponding to the `id` given, in chronological order.
|
||||
- `copy`: Copies the selected address to the clipboard.
|
||||
- `new`: Prompts the user for the option to include a transparent receiver, a Sapling receiver or both. An Orchard receiver is always included.
|
||||
- `send`: Prompts the user to prepare an outgoing transaction, selecting the source address, validating the destination address, the amount and the memo.
|
||||
- If the source is a transparent address, the privacy policy is set to `AllowRevealedSenders`, favoring the shielding of funds when sent to a UA.
|
||||
- If the source is a shielded address, the privacy policy is set to `AllowRevealedAmounts`, favoring the move of funds from legacy shielded pools to Orchard.
|
||||
- `exit`: Ends the session.
|
||||
|
||||
|
2
Setup.hs
Normal file
2
Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Distribution.Simple
|
||||
main = defaultMain
|
157
app/Main.hs
Normal file
157
app/Main.hs
Normal file
|
@ -0,0 +1,157 @@
|
|||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Configurator
|
||||
import Data.Default (def)
|
||||
import Data.Sort
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock.POSIX
|
||||
import System.Console.StructuredCLI
|
||||
import System.Exit
|
||||
import System.IO
|
||||
import Text.Read (readMaybe)
|
||||
import Zenith
|
||||
|
||||
prompt :: String -> IO String
|
||||
prompt text = do
|
||||
putStr text
|
||||
hFlush stdout
|
||||
getLine
|
||||
|
||||
root :: B.ByteString -> B.ByteString -> Commands ()
|
||||
root user pwd = do
|
||||
list user pwd
|
||||
txs user pwd
|
||||
sendZec user pwd
|
||||
copyAdd user pwd
|
||||
createUA user pwd
|
||||
command "exit" "exit app" exitSuccess
|
||||
|
||||
copyAdd :: B.ByteString -> B.ByteString -> Commands ()
|
||||
copyAdd user pwd =
|
||||
command "copy" "copies an address to the clipboard" $ do
|
||||
liftIO . putStrLn $ "Please select the source address:"
|
||||
addList <- listAddresses user pwd
|
||||
let idList = zip [1 ..] addList
|
||||
liftIO $ mapM_ (displayZcashAddress user pwd) idList
|
||||
s <- liftIO . prompt $ " > Enter ID (0 to cancel): "
|
||||
let idx = read s
|
||||
if idx == 0
|
||||
then do
|
||||
liftIO . putStrLn $ " Cancelled!"
|
||||
return NoAction
|
||||
else do
|
||||
liftIO $ copyAddress (addList !! (idx - 1))
|
||||
liftIO . putStrLn $ " Copied address to clipboard!"
|
||||
return NoAction
|
||||
|
||||
list :: B.ByteString -> B.ByteString -> Commands ()
|
||||
list user pwd =
|
||||
command "list" "lists all addresses known to the node" $ do
|
||||
liftIO . putStrLn $ "Addresses known to the node:"
|
||||
liftIO . putStrLn $ "----------------------------"
|
||||
addList <- listAddresses user pwd
|
||||
let idList = zip [1 ..] addList
|
||||
liftIO $ mapM_ (displayZcashAddress user pwd) idList
|
||||
return NoAction
|
||||
|
||||
txs :: B.ByteString -> B.ByteString -> Commands ()
|
||||
txs user pwd =
|
||||
param
|
||||
"txs"
|
||||
"'txs <id>' shows transactions for address <id> from list command"
|
||||
parseId $ \i -> do
|
||||
addList <- listAddresses user pwd
|
||||
liftIO . putStrLn $
|
||||
"Txs for address " ++ T.unpack (addy (addList !! (i - 1))) ++ ":"
|
||||
liftIO . putStrLn $ "----------------------------"
|
||||
txList <- listTxs user pwd (addList !! (i - 1))
|
||||
let txList' = sortOn zblocktime $ filter (not . zchange) txList
|
||||
liftIO $ mapM_ displayTx txList'
|
||||
return NoAction
|
||||
|
||||
sendZec :: B.ByteString -> B.ByteString -> Commands ()
|
||||
sendZec user pwd =
|
||||
command "send" "prompt for sending ZEC" $ do
|
||||
liftIO . putStrLn $ "Please select the source address:"
|
||||
addList <- listAddresses user pwd
|
||||
let idList = zip [1 ..] addList
|
||||
liftIO $ mapM_ (displayZcashAddress user pwd) idList
|
||||
s <- liftIO . prompt $ " > Enter ID (0 to cancel): "
|
||||
let idx = read s
|
||||
if idx == 0
|
||||
then do
|
||||
liftIO . putStrLn $ " Cancelled!"
|
||||
return NoAction
|
||||
else do
|
||||
liftIO . putStrLn $ " Sending from " ++ show (addList !! (idx - 1))
|
||||
t <- liftIO . prompt $ " > Enter destination address: "
|
||||
let addChk = validateAddress (T.pack t)
|
||||
if addChk
|
||||
then do
|
||||
liftIO . putStrLn $ " Address is valid!"
|
||||
a <- liftIO . prompt $ " > Enter ZEC amount: "
|
||||
case (readMaybe a :: Maybe Double) of
|
||||
Just amt -> do
|
||||
m <- liftIO . prompt $ " > Enter memo: "
|
||||
liftIO $ sendTx user pwd (addList !! (idx - 1)) (T.pack t) amt m
|
||||
Nothing -> liftIO . putStrLn $ " Invalid amount"
|
||||
else liftIO . putStrLn $ " Invalid address, cancelling."
|
||||
return NoAction
|
||||
|
||||
createUA :: B.ByteString -> B.ByteString -> Commands ()
|
||||
createUA user pwd =
|
||||
command "new" "create new Unified Address" $ do
|
||||
accCheck <- liftIO $ checkAccounts user pwd
|
||||
if accCheck
|
||||
then do
|
||||
liftIO . putStrLn $ " Account found."
|
||||
else do
|
||||
liftIO . putStrLn $ " No existing accounts, creating one..."
|
||||
liftIO $ createAccount user pwd
|
||||
t <- liftIO . prompt $ " > Include transparent receiver? (Y/N): "
|
||||
let tRec =
|
||||
case T.toLower (T.pack t) of
|
||||
"y" -> True
|
||||
_ -> False
|
||||
s <- liftIO . prompt $ " > Include Sapling receiver? (Y/N): "
|
||||
let sRec =
|
||||
case T.toLower (T.pack s) of
|
||||
"y" -> True
|
||||
_ -> False
|
||||
liftIO $ createUnifiedAddress user pwd tRec sRec
|
||||
return NoAction
|
||||
|
||||
parseId :: Validator IO Int
|
||||
parseId = return . readMaybe
|
||||
|
||||
displayTx :: ZcashTx -> IO ()
|
||||
displayTx t = do
|
||||
putStr "Tx ID: "
|
||||
print $ ztxid t
|
||||
putStr "Block Time: "
|
||||
print $ posixSecondsToUTCTime (fromInteger (zblocktime t))
|
||||
putStr "Zats: "
|
||||
print $ zamountZat t
|
||||
putStr "Memo: "
|
||||
print $ zmemo t
|
||||
putStrLn "-----"
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
config <- load ["zenith.cfg"]
|
||||
nodeUser <- require config "nodeUser"
|
||||
nodePwd <- require config "nodePwd"
|
||||
checkServer nodeUser nodePwd
|
||||
void $
|
||||
runCLI
|
||||
"Zenith"
|
||||
def
|
||||
{ getBanner =
|
||||
" ______ _ _ _ \n |___ / (_) | | | \n / / ___ _ __ _| |_| |__ \n / / / _ \\ '_ \\| | __| '_ \\ \n / /_| __/ | | | | |_| | | |\n /_____\\___|_| |_|_|\\__|_| |_|\n Zcash Full Node CLI"
|
||||
}
|
||||
(root nodeUser nodePwd)
|
1
haskoin
Symbolic link
1
haskoin
Symbolic link
|
@ -0,0 +1 @@
|
|||
/home/rav/Documents/programs/haskoin-core/
|
69
package.yaml
Normal file
69
package.yaml
Normal file
|
@ -0,0 +1,69 @@
|
|||
name: zenith
|
||||
version: 0.1.0.0
|
||||
github: "pitmutt/zenit"
|
||||
license: MIT
|
||||
author: "Rene Vergara"
|
||||
maintainer: "rene@vergara.network"
|
||||
copyright: "Copyright (c) 2022 Vergara Technologies LLC"
|
||||
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- ChangeLog.md
|
||||
|
||||
# Metadata used when publishing your package
|
||||
# synopsis: Short description of your package
|
||||
# category: Web
|
||||
|
||||
# To avoid duplicated efforts in documentation and dealing with the
|
||||
# complications of embedding Haddock markup inside cabal files, it is
|
||||
# common to point users to the README.md file.
|
||||
description: Please see the README on GitLab at <https://gitlab.com/pitmutt/zenit#readme>
|
||||
|
||||
dependencies:
|
||||
- base >= 4.7 && < 5
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
dependencies:
|
||||
- aeson
|
||||
- text
|
||||
- bytestring
|
||||
- http-conduit
|
||||
- scientific
|
||||
- haskoin-core
|
||||
- vector
|
||||
- regex-base
|
||||
- regex-posix
|
||||
- Clipboard
|
||||
- process
|
||||
- http-types
|
||||
|
||||
executables:
|
||||
zenith:
|
||||
main: Main.hs
|
||||
source-dirs: app
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
- -Wall
|
||||
dependencies:
|
||||
- zenith
|
||||
- configurator
|
||||
- structured-cli
|
||||
- data-default
|
||||
- bytestring
|
||||
- text
|
||||
- time
|
||||
- sort
|
||||
|
||||
tests:
|
||||
zenith-test:
|
||||
main: Spec.hs
|
||||
source-dirs: test
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- zenith
|
550
src/Zenith.hs
Normal file
550
src/Zenith.hs
Normal file
|
@ -0,0 +1,550 @@
|
|||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Zenith where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Char
|
||||
import Data.Functor (void)
|
||||
import Data.Maybe
|
||||
import qualified Data.Scientific as Scientific
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import GHC.Generics
|
||||
import Haskoin.Address.Bech32
|
||||
import Network.HTTP.Simple
|
||||
import Network.HTTP.Types
|
||||
import Numeric
|
||||
import System.Clipboard
|
||||
import System.Exit
|
||||
import System.Process (createProcess_, shell)
|
||||
import Text.Regex.Posix
|
||||
|
||||
-- | A type to model Zcash RPC calls
|
||||
data RpcCall =
|
||||
RpcCall
|
||||
{ jsonrpc :: T.Text
|
||||
, id :: T.Text
|
||||
, method :: T.Text
|
||||
, params :: [Value]
|
||||
}
|
||||
deriving (Show, Generic, ToJSON, FromJSON)
|
||||
|
||||
-- | Type for modelling the different address sources for Zcash 5.0.0
|
||||
data AddressSource
|
||||
= LegacyRandom
|
||||
| Imported
|
||||
| ImportedWatchOnly
|
||||
| KeyPool
|
||||
| LegacySeed
|
||||
| MnemonicSeed
|
||||
deriving (Read, Show, Eq, Generic, ToJSON)
|
||||
|
||||
instance FromJSON AddressSource where
|
||||
parseJSON =
|
||||
withText "AddressSource" $ \case
|
||||
"legacy_random" -> return LegacyRandom
|
||||
"imported" -> return Imported
|
||||
"imported_watchonly" -> return ImportedWatchOnly
|
||||
"keypool" -> return KeyPool
|
||||
"legacy_hdseed" -> return LegacySeed
|
||||
"mnemonic_seed" -> return MnemonicSeed
|
||||
_ -> fail "Not a known address source"
|
||||
|
||||
data ZcashPool
|
||||
= Transparent
|
||||
| Sprout
|
||||
| Sapling
|
||||
| Orchard
|
||||
deriving (Show, Eq, Generic, ToJSON)
|
||||
|
||||
instance FromJSON ZcashPool where
|
||||
parseJSON =
|
||||
withText "ZcashPool" $ \case
|
||||
"p2pkh" -> return Transparent
|
||||
"sprout" -> return Sprout
|
||||
"sapling" -> return Sapling
|
||||
"orchard" -> return Orchard
|
||||
_ -> fail "Not a known Zcash pool"
|
||||
|
||||
data ZcashAddress =
|
||||
ZcashAddress
|
||||
{ source :: AddressSource
|
||||
, pool :: [ZcashPool]
|
||||
, account :: Maybe Integer
|
||||
, addy :: T.Text
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
instance Show ZcashAddress where
|
||||
show (ZcashAddress s p i a) =
|
||||
T.unpack (T.take 8 a) ++
|
||||
"..." ++ T.unpack (T.takeEnd 8 a) ++ " Pools: " ++ show p
|
||||
|
||||
-- | A type to model the response of the Zcash RPC
|
||||
data RpcResponse r =
|
||||
RpcResponse
|
||||
{ err :: Maybe T.Text
|
||||
, respId :: T.Text
|
||||
, result :: r
|
||||
}
|
||||
deriving (Show, Generic, ToJSON)
|
||||
|
||||
instance (FromJSON r) => FromJSON (RpcResponse r) where
|
||||
parseJSON (Object obj) = do
|
||||
e <- obj .: "error"
|
||||
rId <- obj .: "id"
|
||||
r <- obj .: "result"
|
||||
pure $ RpcResponse e rId r
|
||||
parseJSON invalid =
|
||||
prependFailure
|
||||
"parsing RpcResponse failed, "
|
||||
(typeMismatch "Object" invalid)
|
||||
|
||||
newtype NodeVersion =
|
||||
NodeVersion Integer
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance FromJSON NodeVersion where
|
||||
parseJSON =
|
||||
withObject "NodeVersion" $ \obj -> do
|
||||
v <- obj .: "version"
|
||||
pure $ NodeVersion v
|
||||
|
||||
-- | A type to model an address group
|
||||
data AddressGroup =
|
||||
AddressGroup
|
||||
{ agsource :: AddressSource
|
||||
, agtransparent :: [ZcashAddress]
|
||||
, agsapling :: [ZcashAddress]
|
||||
, agunified :: [ZcashAddress]
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance FromJSON AddressGroup where
|
||||
parseJSON =
|
||||
withObject "AddressGroup" $ \obj -> do
|
||||
s <- obj .: "source"
|
||||
t <- obj .:? "transparent"
|
||||
sap <- obj .:? "sapling"
|
||||
uni <- obj .:? "unified"
|
||||
sL <- processSapling sap s
|
||||
tL <- processTransparent t s
|
||||
uL <- processUnified uni
|
||||
return $ AddressGroup s tL (concat sL) (concat uL)
|
||||
where
|
||||
processTransparent c s1 =
|
||||
case c of
|
||||
Nothing -> return []
|
||||
Just x -> do
|
||||
x' <- x .: "addresses"
|
||||
return $ map (ZcashAddress s1 [Transparent] Nothing) x'
|
||||
processSapling k s2 =
|
||||
case k of
|
||||
Nothing -> return []
|
||||
Just y -> mapM (processOneSapling s2) y
|
||||
where processOneSapling sx =
|
||||
withObject "Sapling" $ \oS -> do
|
||||
oS' <- oS .: "addresses"
|
||||
return $ map (ZcashAddress sx [Sapling] Nothing) oS'
|
||||
processUnified u =
|
||||
case u of
|
||||
Nothing -> return []
|
||||
Just z -> mapM processOneAccount z
|
||||
where processOneAccount =
|
||||
withObject "UAs" $ \uS -> do
|
||||
acct <- uS .: "account"
|
||||
uS' <- uS .: "addresses"
|
||||
mapM (processUAs acct) uS'
|
||||
where
|
||||
processUAs a =
|
||||
withObject "UAs" $ \v -> do
|
||||
addr <- v .: "address"
|
||||
p <- v .: "receiver_types"
|
||||
return $ ZcashAddress MnemonicSeed p a addr
|
||||
|
||||
-- | A type to model a Zcash transaction
|
||||
data ZcashTx =
|
||||
ZcashTx
|
||||
{ ztxid :: T.Text
|
||||
, zamount :: Double
|
||||
, zamountZat :: Integer
|
||||
, zblockheight :: Integer
|
||||
, zblocktime :: Integer
|
||||
, zchange :: Bool
|
||||
, zconfirmations :: Integer
|
||||
, zmemo :: T.Text
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance FromJSON ZcashTx where
|
||||
parseJSON =
|
||||
withObject "ZcashTx" $ \obj -> do
|
||||
t <- obj .: "txid"
|
||||
a <- obj .: "amount"
|
||||
aZ <- obj .: "amountZat"
|
||||
bh <- obj .: "blockheight"
|
||||
bt <- obj .: "blocktime"
|
||||
c <- obj .: "change"
|
||||
conf <- obj .: "confirmations"
|
||||
m <- obj .:? "memo"
|
||||
pure $
|
||||
ZcashTx
|
||||
t
|
||||
a
|
||||
aZ
|
||||
bh
|
||||
bt
|
||||
c
|
||||
conf
|
||||
(case m of
|
||||
Nothing -> ""
|
||||
Just m' -> T.pack (filter (/= '\NUL') $ decodeHexText m'))
|
||||
|
||||
instance ToJSON ZcashTx where
|
||||
toJSON (ZcashTx t a aZ bh bt c conf m) =
|
||||
object
|
||||
[ "amount" .= a
|
||||
, "amountZat" .= aZ
|
||||
, "txid" .= t
|
||||
, "blockheight" .= bh
|
||||
, "blocktime" .= bt
|
||||
, "change" .= c
|
||||
, "confirmations" .= conf
|
||||
, "memo" .= m
|
||||
]
|
||||
|
||||
-- | Type for the UA balance
|
||||
data UABalance =
|
||||
UABalance
|
||||
{ uatransparent :: Integer
|
||||
, uasapling :: Integer
|
||||
, uaorchard :: Integer
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
instance Show UABalance where
|
||||
show (UABalance t s o) =
|
||||
" T: " ++ show t ++ " S: " ++ show s ++ " O: " ++ show o
|
||||
|
||||
instance FromJSON UABalance where
|
||||
parseJSON =
|
||||
withObject "UABalance" $ \obj -> do
|
||||
p <- obj .: "pools"
|
||||
t <- p .:? "transparent"
|
||||
s <- p .:? "sapling"
|
||||
o <- p .:? "orchard"
|
||||
vT <-
|
||||
case t of
|
||||
Nothing -> return 0
|
||||
Just t' -> t' .: "valueZat"
|
||||
vS <-
|
||||
case s of
|
||||
Nothing -> return 0
|
||||
Just s' -> s' .: "valueZat"
|
||||
vO <-
|
||||
case o of
|
||||
Nothing -> return 0
|
||||
Just o' -> o' .: "valueZat"
|
||||
pure $ UABalance vT vS vO
|
||||
|
||||
-- | Type for Operation Result
|
||||
data OpResult =
|
||||
OpResult
|
||||
{ opsuccess :: Bool
|
||||
, opmessage :: Maybe T.Text
|
||||
, optxid :: Maybe T.Text
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance FromJSON OpResult where
|
||||
parseJSON =
|
||||
withObject "OpResult" $ \obj -> do
|
||||
s <- obj .: "status"
|
||||
let s' = s == ("success" :: String)
|
||||
r <- obj .:? "result"
|
||||
e <- obj .:? "error"
|
||||
t <-
|
||||
case r of
|
||||
Nothing -> return Nothing
|
||||
Just r' -> r' .: "txid"
|
||||
m <-
|
||||
case e of
|
||||
Nothing -> return Nothing
|
||||
Just m' -> m' .: "message"
|
||||
pure $ OpResult s' m t
|
||||
|
||||
-- | Helper function to turn a hex-encoded memo strings to readable text
|
||||
decodeHexText :: String -> String
|
||||
decodeHexText hexText
|
||||
| null chunk = ""
|
||||
| otherwise = chr (read ("0x" <> chunk)) : decodeHexText (drop 2 hexText)
|
||||
where
|
||||
chunk = take 2 hexText
|
||||
|
||||
-- | Helper function to turn a string into a hex-encoded string
|
||||
encodeHexText :: String -> String
|
||||
encodeHexText t = mconcat (map padHex t)
|
||||
where
|
||||
padHex x =
|
||||
if ord x < 16
|
||||
then "0" ++ (showHex . ord) x ""
|
||||
else showHex (ord x) ""
|
||||
|
||||
-- | Helper function to extract addresses from AddressGroups
|
||||
getAddresses :: AddressGroup -> [ZcashAddress]
|
||||
getAddresses ag = agtransparent ag <> agsapling ag <> agunified ag
|
||||
|
||||
-- | Helper function to validate potential Zcash addresses
|
||||
validateAddress :: T.Text -> Bool
|
||||
validateAddress txt = (tReg || sReg && isJust chk) || (uReg && isJust chk)
|
||||
where
|
||||
transparentRegex = "^t1[a-zA-Z0-9]{33}$" :: String
|
||||
shieldedRegex = "^zs[a-zA-Z0-9]{76}$" :: String
|
||||
unifiedRegex = "^u[a-zA-Z0-9]" :: String
|
||||
tReg = T.unpack txt =~ transparentRegex :: Bool
|
||||
sReg = T.unpack txt =~ shieldedRegex :: Bool
|
||||
uReg = T.unpack txt =~ unifiedRegex :: Bool
|
||||
chk = bech32mDecode txt
|
||||
|
||||
-- | RPC methods
|
||||
-- | List addresses
|
||||
listAddresses :: B.ByteString -> B.ByteString -> IO [ZcashAddress]
|
||||
listAddresses user pwd = do
|
||||
response <- makeZcashCall user pwd "listaddresses" []
|
||||
let rpcResp = decode response :: Maybe (RpcResponse [AddressGroup])
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
let addys = result res
|
||||
let addList = concatMap getAddresses addys
|
||||
return addList
|
||||
|
||||
-- | Get address balance
|
||||
getBalance :: B.ByteString -> B.ByteString -> ZcashAddress -> IO [Integer]
|
||||
getBalance user pwd zadd = do
|
||||
let a = account zadd
|
||||
case a of
|
||||
Nothing -> do
|
||||
response <-
|
||||
makeZcashCall
|
||||
user
|
||||
pwd
|
||||
"z_getbalance"
|
||||
[ String (addy zadd)
|
||||
, Number (Scientific.scientific 1 0)
|
||||
, Data.Aeson.Bool True
|
||||
]
|
||||
let rpcResp = decode response :: Maybe (RpcResponse Integer)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
return [result res]
|
||||
Just acct -> do
|
||||
response <-
|
||||
makeZcashCall
|
||||
user
|
||||
pwd
|
||||
"z_getbalanceforaccount"
|
||||
[Number (Scientific.scientific acct 0)]
|
||||
let rpcResp = decode response :: Maybe (RpcResponse UABalance)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
return $ readUABalance (result res)
|
||||
where readUABalance ua =
|
||||
[uatransparent ua, uasapling ua, uaorchard ua]
|
||||
|
||||
-- | List transactions
|
||||
listTxs :: B.ByteString -> B.ByteString -> ZcashAddress -> IO [ZcashTx]
|
||||
listTxs user pwd zaddy = do
|
||||
response <-
|
||||
makeZcashCall user pwd "z_listreceivedbyaddress" [String $ addy zaddy]
|
||||
let rpcResp = decode response :: Maybe (RpcResponse [ZcashTx])
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
return $ result res
|
||||
|
||||
-- | Send Tx
|
||||
sendTx ::
|
||||
B.ByteString
|
||||
-> B.ByteString
|
||||
-> ZcashAddress
|
||||
-> T.Text
|
||||
-> Double
|
||||
-> String
|
||||
-> IO ()
|
||||
sendTx user pwd fromAddy toAddy amount memo = do
|
||||
bal <- getBalance user pwd fromAddy
|
||||
if sum bal - floor (amount * 100000000) >= 1000
|
||||
then do
|
||||
if source fromAddy /= ImportedWatchOnly
|
||||
then do
|
||||
let privacyPolicy =
|
||||
if isNothing (account fromAddy) &&
|
||||
elem Transparent (pool fromAddy)
|
||||
then "AllowRevealedSenders"
|
||||
else "AllowRevealedAmounts"
|
||||
let pd =
|
||||
[ Data.Aeson.String (addy fromAddy)
|
||||
, Data.Aeson.Array
|
||||
(V.fromList
|
||||
[ object
|
||||
[ "address" .= toAddy
|
||||
, "amount" .= amount
|
||||
, "memo" .= encodeHexText memo
|
||||
]
|
||||
])
|
||||
, Data.Aeson.Number $ Scientific.scientific 1 1
|
||||
, Data.Aeson.Number $ Scientific.scientific 1 (-5)
|
||||
, Data.Aeson.String privacyPolicy
|
||||
]
|
||||
response <- makeZcashCall user pwd "z_sendmany" pd
|
||||
let rpcResp = decode response :: Maybe (RpcResponse T.Text)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
putStrLn " Sending...."
|
||||
threadDelay 10000000 >> checkOpResult user pwd (result res)
|
||||
else putStrLn "Error: Source address is view-only."
|
||||
else putStrLn "Error: Insufficient balance in source address."
|
||||
|
||||
-- | Make a Zcash RPC call
|
||||
makeZcashCall ::
|
||||
B.ByteString
|
||||
-> B.ByteString
|
||||
-> T.Text
|
||||
-> [Data.Aeson.Value]
|
||||
-> IO LB.ByteString
|
||||
makeZcashCall username password m p = do
|
||||
let payload = RpcCall "1.0" "test" m p
|
||||
let myRequest =
|
||||
setRequestBodyJSON payload $
|
||||
setRequestPort 8232 $
|
||||
setRequestBasicAuth username password $
|
||||
setRequestMethod "POST" defaultRequest
|
||||
response <- httpLBS myRequest
|
||||
let respStatus = getResponseStatusCode response
|
||||
let body = getResponseBody response
|
||||
case respStatus of
|
||||
500 -> do
|
||||
let rpcResp = decode body :: Maybe (RpcResponse String)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Unknown server error"
|
||||
Just x -> fail (result x)
|
||||
401 -> fail "Incorrect full node credentials"
|
||||
200 -> return body
|
||||
_ -> fail "Unknown error"
|
||||
|
||||
-- | Display an address
|
||||
displayZcashAddress ::
|
||||
B.ByteString -> B.ByteString -> (Int, ZcashAddress) -> IO ()
|
||||
displayZcashAddress user pwd (idx, zaddy) = do
|
||||
zats <- getBalance user pwd zaddy
|
||||
putStr $ show idx ++ ": "
|
||||
putStr $ show zaddy
|
||||
putStr " Zats: "
|
||||
print zats
|
||||
|
||||
-- | Copy an address to the clipboard
|
||||
copyAddress :: ZcashAddress -> IO ()
|
||||
copyAddress a =
|
||||
void $
|
||||
createProcess_ "toClipboard" $
|
||||
shell $ "echo " ++ T.unpack (addy a) ++ " | xclip -r -selection clipboard"
|
||||
|
||||
-- | Verify operation result
|
||||
checkOpResult :: B.ByteString -> B.ByteString -> T.Text -> IO ()
|
||||
checkOpResult user pwd opid = do
|
||||
response <-
|
||||
makeZcashCall
|
||||
user
|
||||
pwd
|
||||
"z_getoperationstatus"
|
||||
[Data.Aeson.Array (V.fromList [Data.Aeson.String opid])]
|
||||
let rpcResp = decode response :: Maybe (RpcResponse [OpResult])
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
let r = result res
|
||||
mapM_ showResult r
|
||||
where
|
||||
showResult t =
|
||||
if opsuccess t
|
||||
then putStrLn $ " Success! Tx ID: " ++ maybe "" T.unpack (optxid t)
|
||||
else putStrLn $ " Failed :( " ++ maybe "" T.unpack (opmessage t)
|
||||
|
||||
-- | Check for accounts
|
||||
checkAccounts :: B.ByteString -> B.ByteString -> IO Bool
|
||||
checkAccounts user pwd = do
|
||||
response <- makeZcashCall user pwd "z_listaccounts" []
|
||||
let rpcResp = decode response :: Maybe (RpcResponse [Object])
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
let r = result res
|
||||
return $ not (null r)
|
||||
|
||||
-- | Add account to node
|
||||
createAccount :: B.ByteString -> B.ByteString -> IO ()
|
||||
createAccount user pwd = do
|
||||
response <- makeZcashCall user pwd "z_getnewaccount" []
|
||||
let rpcResp = decode response :: Maybe (RpcResponse Object)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
let r = result res
|
||||
putStrLn " Account created!"
|
||||
|
||||
-- | Create new Unified Address
|
||||
createUnifiedAddress :: B.ByteString -> B.ByteString -> Bool -> Bool -> IO ()
|
||||
createUnifiedAddress user pwd tRec sRec = do
|
||||
let recs = getReceivers tRec sRec
|
||||
let pd = [Data.Aeson.Number $ Scientific.scientific 0 1, recs]
|
||||
newResp <- makeZcashCall user pwd "z_getaddressforaccount" pd
|
||||
let rpcResp = decode newResp :: Maybe (RpcResponse Object)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just res -> do
|
||||
let r = result res
|
||||
putStrLn " New UA created!"
|
||||
where
|
||||
getReceivers t s
|
||||
| t && s =
|
||||
Data.Aeson.Array
|
||||
(V.fromList
|
||||
[ Data.Aeson.String "p2pkh"
|
||||
, Data.Aeson.String "sapling"
|
||||
, Data.Aeson.String "orchard"
|
||||
])
|
||||
| t =
|
||||
Data.Aeson.Array
|
||||
(V.fromList [Data.Aeson.String "p2pkh", Data.Aeson.String "orchard"])
|
||||
| s =
|
||||
Data.Aeson.Array
|
||||
(V.fromList [Data.Aeson.String "sapling", Data.Aeson.String "orchard"])
|
||||
| otherwise = Data.Aeson.Array (V.fromList [Data.Aeson.String "orchard"])
|
||||
|
||||
-- | Check Zcash full node server
|
||||
checkServer :: B.ByteString -> B.ByteString -> IO ()
|
||||
checkServer user pwd = do
|
||||
resp <- makeZcashCall user pwd "getinfo" []
|
||||
let rpcResp = decode resp :: Maybe (RpcResponse NodeVersion)
|
||||
case rpcResp of
|
||||
Nothing -> fail "Couldn't parse node response"
|
||||
Just myResp -> do
|
||||
let r = result myResp
|
||||
if isNodeValid r
|
||||
then putStrLn "Connected to Zcash Full Node :)"
|
||||
else do
|
||||
putStrLn "Deprecated Zcash Full Node version found. Exiting"
|
||||
exitFailure
|
||||
where isNodeValid (NodeVersion i) = i >= 5000000
|
68
stack.yaml
Normal file
68
stack.yaml
Normal file
|
@ -0,0 +1,68 @@
|
|||
# This file was automatically generated by 'stack init'
|
||||
#
|
||||
# Some commonly used options have been documented as comments in this file.
|
||||
# For advanced use and comprehensive documentation of the format, please see:
|
||||
# https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
|
||||
# A snapshot resolver dictates the compiler version and the set of packages
|
||||
# to be used for project dependencies. For example:
|
||||
#
|
||||
# resolver: lts-3.5
|
||||
# resolver: nightly-2015-09-21
|
||||
# resolver: ghc-7.10.2
|
||||
#
|
||||
# The location of a snapshot can be provided as a file or url. Stack assumes
|
||||
# a snapshot provided as a file might change, whereas a url resource does not.
|
||||
#
|
||||
# resolver: ./custom-snapshot.yaml
|
||||
# resolver: https://example.com/snapshots/2018-01-01.yaml
|
||||
resolver:
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/9.yaml
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
#
|
||||
# packages:
|
||||
# - some-directory
|
||||
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||
# subdirs:
|
||||
# - auto-update
|
||||
# - wai
|
||||
packages:
|
||||
- .
|
||||
- haskoin
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver.
|
||||
# These entries can reference officially published versions as well as
|
||||
# forks / in-progress versions pinned to a git hash. For example:
|
||||
#
|
||||
# extra-deps:
|
||||
# - acme-missiles-0.3
|
||||
# - git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
#
|
||||
# extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
# flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
# extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
#
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: ">=2.7"
|
||||
#
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
#
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
#extra-lib-dirs: [/home/rav/Documents/programs/haskoin]
|
||||
#
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
13
stack.yaml.lock
Normal file
13
stack.yaml.lock
Normal file
|
@ -0,0 +1,13 @@
|
|||
# This file was autogenerated by Stack.
|
||||
# You should not edit this file by hand.
|
||||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/lock_files
|
||||
|
||||
packages: []
|
||||
snapshots:
|
||||
- completed:
|
||||
size: 618507
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/9.yaml
|
||||
sha256: 190a93d09d9d7bccc78a0f00d6fc0350eed76bc533611e5971202800805dd00e
|
||||
original:
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/9.yaml
|
2
test/Spec.hs
Normal file
2
test/Spec.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
main :: IO ()
|
||||
main = putStrLn "Test suite not yet implemented"
|
79
zenith.cabal
Normal file
79
zenith.cabal
Normal file
|
@ -0,0 +1,79 @@
|
|||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.34.4.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: zenith
|
||||
version: 0.1.0.0
|
||||
description: Please see the README on GitLab at <https://gitlab.com/pitmutt/zenit#readme>
|
||||
homepage: https://github.com/pitmutt/zenit#readme
|
||||
bug-reports: https://github.com/pitmutt/zenit/issues
|
||||
author: Rene Vergara
|
||||
maintainer: rene@vergara.network
|
||||
copyright: Copyright (c) 2022 Vergara Technologies LLC
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
ChangeLog.md
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/pitmutt/zenit
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Zenith
|
||||
other-modules:
|
||||
Paths_zenith
|
||||
hs-source-dirs:
|
||||
src
|
||||
build-depends:
|
||||
Clipboard
|
||||
, aeson
|
||||
, base >=4.7 && <5
|
||||
, bytestring
|
||||
, haskoin-core
|
||||
, http-conduit
|
||||
, http-types
|
||||
, process
|
||||
, regex-base
|
||||
, regex-posix
|
||||
, scientific
|
||||
, text
|
||||
, vector
|
||||
default-language: Haskell2010
|
||||
|
||||
executable zenith
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_zenith
|
||||
hs-source-dirs:
|
||||
app
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
|
||||
build-depends:
|
||||
base >=4.7 && <5
|
||||
, bytestring
|
||||
, configurator
|
||||
, data-default
|
||||
, sort
|
||||
, structured-cli
|
||||
, text
|
||||
, time
|
||||
, zenith
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite zenith-test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Spec.hs
|
||||
other-modules:
|
||||
Paths_zenith
|
||||
hs-source-dirs:
|
||||
test
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
build-depends:
|
||||
base >=4.7 && <5
|
||||
, zenith
|
||||
default-language: Haskell2010
|
2
zenith.cfg
Normal file
2
zenith.cfg
Normal file
|
@ -0,0 +1,2 @@
|
|||
nodeUser = "user"
|
||||
nodePwd = "superSecret"
|
Loading…
Reference in a new issue