Unified Address support #8

Merged
pitmutt merged 61 commits from dev18 into master 2023-10-28 12:24:28 +00:00
11 changed files with 1085 additions and 471 deletions

View file

@ -4,6 +4,27 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.8.0]
### Added
- Parser for Unified Addresses that validates the address
- Tests for UA parsing from wallets
- Function to scan new transactions using known viewing keys
- Function to identify the owners and VKs needed for tx scans
### Changed
- Order endpoint updated to ensure orders belong to shop before adding to DB.
- MongoDB driver updated to support MongoDB 6.
- Full validation of Sapling addresses to parser.
### Removed
- `api/orderx` endpoint.
- `makeZcashCall` function moved to the generic `zcash-haskell` library.
- `RpcResponse`, `RpcCall` types moved to the generic `zcash-haskell` library.
## [1.7.0] ## [1.7.0]
### Added ### Added

View file

@ -23,10 +23,12 @@ main = do
putStrLn "Connected to MongoDB!" putStrLn "Connected to MongoDB!"
checkZcashPrices pipe (c_dbName loadedConfig) checkZcashPrices pipe (c_dbName loadedConfig)
scanZcash' loadedConfig pipe scanZcash' loadedConfig pipe
scanPayments loadedConfig pipe {-scanPayments loadedConfig pipe-}
scanTxNative loadedConfig pipe
checkPayments pipe (c_dbName loadedConfig) checkPayments pipe (c_dbName loadedConfig)
expireOwners pipe (c_dbName loadedConfig) expireOwners pipe (c_dbName loadedConfig)
updateLogins pipe loadedConfig updateLogins pipe loadedConfig
expireProSessions pipe (c_dbName loadedConfig) expireProSessions pipe (c_dbName loadedConfig)
loadTranslations pipe loadedConfig
close pipe close pipe
else fail "MongoDB connection failed!" else fail "MongoDB connection failed!"

View file

@ -1,5 +1,5 @@
name: zgo-backend name: zgo-backend
version: 1.7.0 version: 1.8.0
git: "https://git.vergara.tech/Vergara_Tech/zgo-backend" git: "https://git.vergara.tech/Vergara_Tech/zgo-backend"
license: BOSL license: BOSL
author: "Rene Vergara" author: "Rene Vergara"

View file

@ -12,29 +12,31 @@ import Data.Time.Clock
import Database.MongoDB import Database.MongoDB
import GHC.Generics import GHC.Generics
import Test.QuickCheck import Test.QuickCheck
import WooCommerce (WooToken(w_id))
-- | Type to represent a ZGo order -- | Type to represent a ZGo order
data ZGoOrder = data ZGoOrder = ZGoOrder
ZGoOrder { q_id :: Maybe ObjectId
{ q_id :: Maybe ObjectId , qaddress :: T.Text
, qaddress :: T.Text , qsession :: T.Text
, qsession :: T.Text , qtimestamp :: UTCTime
, qtimestamp :: UTCTime , qclosed :: Bool
, qclosed :: Bool , qcurrency :: T.Text
, qcurrency :: T.Text , qprice :: Double
, qprice :: Double , qtotal :: Double
, qtotal :: Double , qtotalZec :: Double
, qtotalZec :: Double , qlines :: [LineItem]
, qlines :: [LineItem] , qpaid :: Bool
, qpaid :: Bool , qexternalInvoice :: T.Text
, qexternalInvoice :: T.Text , qshortCode :: T.Text
, qshortCode :: T.Text , qtoken :: T.Text
, qtoken :: T.Text , qtax :: Double
} , qvat :: Double
deriving (Eq, Show, Generic) , qtip :: Double
} deriving (Eq, Show, Generic)
instance ToJSON ZGoOrder where instance ToJSON ZGoOrder where
toJSON (ZGoOrder i a s ts c cur p t tZ l paid eI sC tk) = toJSON (ZGoOrder i a s ts c cur p t tZ l paid eI sC tk qT qV tip) =
case i of case i of
Just oid -> Just oid ->
object object
@ -52,6 +54,9 @@ instance ToJSON ZGoOrder where
, "externalInvoice" .= eI , "externalInvoice" .= eI
, "shortCode" .= sC , "shortCode" .= sC
, "token" .= tk , "token" .= tk
, "taxAmount" .= qT
, "vatAmount" .= qV
, "tipAmount" .= tip
] ]
Nothing -> Nothing ->
object object
@ -69,6 +74,9 @@ instance ToJSON ZGoOrder where
, "externalInvoice" .= eI , "externalInvoice" .= eI
, "shortCode" .= sC , "shortCode" .= sC
, "token" .= tk , "token" .= tk
, "taxAmount" .= qT
, "vatAmount" .= qV
, "tipAmount" .= tip
] ]
instance FromJSON ZGoOrder where instance FromJSON ZGoOrder where
@ -88,10 +96,13 @@ instance FromJSON ZGoOrder where
eI <- obj .: "externalInvoice" eI <- obj .: "externalInvoice"
sC <- obj .: "shortCode" sC <- obj .: "shortCode"
tk <- obj .: "token" tk <- obj .: "token"
qT <- obj .: "taxAmount"
qV <- obj .: "vatAmount"
tip <- obj .: "tipAmount"
pure $ pure $
ZGoOrder ZGoOrder
(if not (null i) (if not (null i)
then Just (read i) then Just (read i :: ObjectId)
else Nothing) else Nothing)
a a
s s
@ -106,9 +117,12 @@ instance FromJSON ZGoOrder where
eI eI
sC sC
tk tk
qT
qV
tip
instance Val ZGoOrder where instance Val ZGoOrder where
val (ZGoOrder i a s ts c cur p t tZ l pd eI sC tk) = val (ZGoOrder i a s ts c cur p t tZ l pd eI sC tk qT qV tip) =
if isJust i if isJust i
then Doc then Doc
[ "_id" =: i [ "_id" =: i
@ -125,6 +139,9 @@ instance Val ZGoOrder where
, "externalInvoice" =: eI , "externalInvoice" =: eI
, "shortCode" =: sC , "shortCode" =: sC
, "token" =: tk , "token" =: tk
, "taxAmount" =: qT
, "vatAmount" =: qV
, "tipAmount" =: tip
] ]
else Doc else Doc
[ "address" =: a [ "address" =: a
@ -140,6 +157,9 @@ instance Val ZGoOrder where
, "externalInvoice" =: eI , "externalInvoice" =: eI
, "shortCode" =: sC , "shortCode" =: sC
, "token" =: tk , "token" =: tk
, "taxAmount" =: qT
, "vatAmount" =: qV
, "tipAmount" =: tip
] ]
cast' (Doc d) = do cast' (Doc d) = do
i <- B.lookup "_id" d i <- B.lookup "_id" d
@ -156,17 +176,18 @@ instance Val ZGoOrder where
eI <- B.lookup "externalInvoice" d eI <- B.lookup "externalInvoice" d
sC <- B.lookup "shortCode" d sC <- B.lookup "shortCode" d
tk <- B.lookup "token" d tk <- B.lookup "token" d
Just (ZGoOrder i a s ts c cur p t tZ l pd eI sC tk) qT <- B.lookup "taxAmount" d
qV <- B.lookup "vatAmount" d
tip <- B.lookup "tipAmount" d
Just (ZGoOrder i a s ts c cur p t tZ l pd eI sC tk qT qV tip)
cast' _ = Nothing cast' _ = Nothing
-- Type to represent an order line item -- Type to represent an order line item
data LineItem = data LineItem = LineItem
LineItem { lqty :: Double
{ lqty :: Double , lname :: T.Text
, lname :: T.Text , lcost :: Double
, lcost :: Double } deriving (Eq, Show)
}
deriving (Eq, Show)
instance ToJSON LineItem where instance ToJSON LineItem where
toJSON (LineItem q n c) = object ["qty" .= q, "name" .= n, "cost" .= c] toJSON (LineItem q n c) = object ["qty" .= q, "name" .= n, "cost" .= c]
@ -189,33 +210,40 @@ instance Val LineItem where
cast' _ = Nothing cast' _ = Nothing
-- Database actions -- Database actions
upsertOrder :: ZGoOrder -> Action IO () upsertOrder :: ZGoOrder -> Double -> Double -> Action IO ()
upsertOrder o = do upsertOrder o taxRate vatRate = do
let order = val $ updateOrderTotals o let order = val $ updateOrderTotals o taxRate vatRate
case order of case order of
Doc d -> Doc d ->
if isJust (q_id o) if isJust (q_id o)
then upsert (select ["_id" =: q_id o] "orders") d then upsert (select ["_id" =: q_id o] "orders") d
else insert_ "orders" d else insert_ "orders" d
_ -> return () _ -> return ()
insertWooOrder :: ZGoOrder -> Action IO Database.MongoDB.Value insertWooOrder :: ZGoOrder -> Action IO Database.MongoDB.Value
insertWooOrder o = do insertWooOrder o = do
let order = val $ updateOrderTotals o let order = val $ updateOrderTotals o 0 0
case order of case order of
Doc d -> insert "orders" d Doc d -> insert "orders" d
_ -> fail "Couldn't parse order" _ -> fail "Couldn't parse order"
upsertXeroOrder :: ZGoOrder -> Action IO () upsertXeroOrder :: ZGoOrder -> Action IO ()
upsertXeroOrder o = do upsertXeroOrder o = do
let order = val $ updateOrderTotals o let order = val $ updateOrderTotals o 0 0
case order of case order of
Doc d -> upsert (select ["externalInvoice" =: qexternalInvoice o, "shortCode" =: qshortCode o] "orders") d Doc d ->
upsert
(select
[ "externalInvoice" =: qexternalInvoice o
, "shortCode" =: qshortCode o
]
"orders")
d
_ -> return () _ -> return ()
-- | Function to update order totals from items -- | Function to update order totals from items
updateOrderTotals :: ZGoOrder -> ZGoOrder updateOrderTotals :: ZGoOrder -> Double -> Double -> ZGoOrder
updateOrderTotals o = updateOrderTotals o taxRate vatRate =
ZGoOrder ZGoOrder
(q_id o) (q_id o)
(qaddress o) (qaddress o)
@ -224,36 +252,51 @@ updateOrderTotals o =
(qclosed o) (qclosed o)
(qcurrency o) (qcurrency o)
(qprice o) (qprice o)
(newTotal o) (newTotal o taxRate vatRate)
(if qprice o /= 0 (if qprice o /= 0
then roundZec (newTotal o / qprice o) then roundZec (newTotal o taxRate vatRate / qprice o)
else 0) else 0)
(qlines o) (qlines o)
(qpaid o) (qpaid o)
(qexternalInvoice o) (qexternalInvoice o)
(qshortCode o) (qshortCode o)
(qtoken o) (qtoken o)
(updateTax o taxRate)
(updateTax o vatRate)
(qtip o)
where where
newTotal :: ZGoOrder -> Double updateTax :: ZGoOrder -> Double -> Double
newTotal x = foldr tallyItems 0 (qlines x) updateTax x t = roundFiat $ itemsTotal (qlines x) * t / 100.0
itemsTotal :: [LineItem] -> Double
itemsTotal = foldr tallyItems 0
newTotal :: ZGoOrder -> Double -> Double -> Double
newTotal x tR vR =
itemsTotal (qlines x) + updateTax x tR + updateTax x vR + qtip x
tallyItems :: LineItem -> Double -> Double tallyItems :: LineItem -> Double -> Double
tallyItems y z = (lqty y * lcost y) + z tallyItems y z = (lqty y * lcost y) + z
setOrderToken :: T.Text -> ZGoOrder -> ZGoOrder setOrderToken :: T.Text -> ZGoOrder -> ZGoOrder
setOrderToken token (ZGoOrder i a s ts c cur p t tZ l pd eI sC tk) = setOrderToken token (ZGoOrder i a s ts c cur p t tZ l pd eI sC tk qT qV tip) =
ZGoOrder i a s ts c cur p t tZ l pd eI sC token ZGoOrder i a s ts c cur p t tZ l pd eI sC token qT qV tip
findOrder :: T.Text -> Action IO (Maybe Document) findOrder :: T.Text -> Action IO (Maybe Document)
findOrder s = findOne (select ["session" =: s, "closed" =: False] "orders") findOrder s = findOne (select ["session" =: s, "closed" =: False] "orders")
findXeroOrder :: T.Text -> T.Text -> T.Text -> Action IO (Maybe Document) findXeroOrder :: T.Text -> T.Text -> T.Text -> Action IO (Maybe Document)
findXeroOrder a i s = findOne (select ["address" =: a, "externalInvoice" =: i, "shortCode" =: s] "orders") findXeroOrder a i s =
findOne
(select ["address" =: a, "externalInvoice" =: i, "shortCode" =: s] "orders")
findOrderById :: String -> Action IO (Maybe Document) findOrderById :: String -> Action IO (Maybe Document)
findOrderById "0" = return Nothing
findOrderById i = findOne (select ["_id" =: (read i :: B.ObjectId)] "orders") findOrderById i = findOne (select ["_id" =: (read i :: B.ObjectId)] "orders")
findAllOrders :: T.Text -> Action IO [Document] findAllOrders :: T.Text -> Action IO [Document]
findAllOrders a = rest =<< find (select ["address" =: a] "orders") {sort = ["timestamp" =: (negate 1 :: Int)]} findAllOrders a =
rest =<<
find
(select ["address" =: a] "orders")
{sort = ["timestamp" =: (negate 1 :: Int)]}
deleteOrder :: String -> Action IO () deleteOrder :: String -> Action IO ()
deleteOrder i = deleteOne (select ["_id" =: (read i :: B.ObjectId)] "orders") deleteOrder i = deleteOne (select ["_id" =: (read i :: B.ObjectId)] "orders")
@ -268,3 +311,6 @@ markOrderPaid (i, a) = do
-- | Helper function to round to 8 decimal places -- | Helper function to round to 8 decimal places
roundZec :: Double -> Double roundZec :: Double -> Double
roundZec n = fromInteger (round $ n * (10 ^ 8)) / (10.0 ^^ 8) roundZec n = fromInteger (round $ n * (10 ^ 8)) / (10.0 ^^ 8)
roundFiat :: Double -> Double
roundFiat n = fromInteger (round $ n * (10 ^ 2)) / (10.0 ^^ 2)

View file

@ -14,38 +14,37 @@ import Database.MongoDB
import GHC.Generics import GHC.Generics
-- | Type to represent a ZGo shop owner/business -- | Type to represent a ZGo shop owner/business
data Owner = data Owner = Owner
Owner { o_id :: Maybe ObjectId
{ o_id :: Maybe ObjectId , oaddress :: T.Text
, oaddress :: T.Text , oname :: T.Text
, oname :: T.Text , ocurrency :: T.Text
, ocurrency :: T.Text , otax :: Bool
, otax :: Bool , otaxValue :: Double
, otaxValue :: Double , ovat :: Bool
, ovat :: Bool , ovatValue :: Double
, ovatValue :: Double , ofirst :: T.Text
, ofirst :: T.Text , olast :: T.Text
, olast :: T.Text , oemail :: T.Text
, oemail :: T.Text , ostreet :: T.Text
, ostreet :: T.Text , ocity :: T.Text
, ocity :: T.Text , ostate :: T.Text
, ostate :: T.Text , opostal :: T.Text
, opostal :: T.Text , ophone :: T.Text
, ophone :: T.Text , owebsite :: T.Text
, owebsite :: T.Text , ocountry :: T.Text
, ocountry :: T.Text , opaid :: Bool
, opaid :: Bool , ozats :: Bool
, ozats :: Bool , oinvoices :: Bool
, oinvoices :: Bool , oexpiration :: UTCTime
, oexpiration :: UTCTime , opayconf :: Bool
, opayconf :: Bool , oviewkey :: T.Text
, oviewkey :: T.Text , ocrmToken :: T.Text
, ocrmToken :: T.Text , otips :: Bool
} } deriving (Eq, Show, Generic, Typeable)
deriving (Eq, Show, Generic, Typeable)
instance ToJSON Owner where instance ToJSON Owner where
toJSON (Owner i a n c t tV v vV f l e s ct st p ph w co paid zats inv eTs pc vk cT) = toJSON (Owner i a n c t tV v vV f l e s ct st p ph w co paid zats inv eTs pc vk cT oT) =
case i of case i of
Just oid -> Just oid ->
object object
@ -74,6 +73,7 @@ instance ToJSON Owner where
, "payconf" .= pc , "payconf" .= pc
, "viewkey" .= vk , "viewkey" .= vk
, "crmToken" .= cT , "crmToken" .= cT
, "tips" .= oT
] ]
Nothing -> Nothing ->
object object
@ -102,6 +102,7 @@ instance ToJSON Owner where
, "payconf" .= pc , "payconf" .= pc
, "viewkey" .= vk , "viewkey" .= vk
, "crmToken" .= cT , "crmToken" .= cT
, "tips" .= oT
] ]
instance FromJSON Owner where instance FromJSON Owner where
@ -132,6 +133,7 @@ instance FromJSON Owner where
pc <- obj .:? "payconf" pc <- obj .:? "payconf"
vk <- obj .:? "viewkey" vk <- obj .:? "viewkey"
cT <- obj .:? "crmToken" cT <- obj .:? "crmToken"
oT <- obj .:? "tips"
pure $ pure $
Owner Owner
(if not (null i) (if not (null i)
@ -161,6 +163,7 @@ instance FromJSON Owner where
(fromMaybe False pc) (fromMaybe False pc)
(fromMaybe "" vk) (fromMaybe "" vk)
(fromMaybe "" cT) (fromMaybe "" cT)
(fromMaybe False oT)
instance Val Owner where instance Val Owner where
cast' (Doc d) = do cast' (Doc d) = do
@ -189,6 +192,7 @@ instance Val Owner where
pc <- B.lookup "payconf" d pc <- B.lookup "payconf" d
vk <- B.lookup "viewKey" d vk <- B.lookup "viewKey" d
cT <- B.lookup "crmToken" d cT <- B.lookup "crmToken" d
oT <- B.lookup "tips" d
Just Just
(Owner (Owner
i i
@ -215,9 +219,10 @@ instance Val Owner where
ets ets
pc pc
vk vk
cT) cT
oT)
cast' _ = Nothing cast' _ = Nothing
val (Owner i a n c t tV v vV f l e s ct st p ph w co paid zats inv ets pc vk cT) = val (Owner i a n c t tV v vV f l e s ct st p ph w co paid zats inv ets pc vk cT oT) =
case i of case i of
Just oid -> Just oid ->
Doc Doc
@ -246,6 +251,7 @@ instance Val Owner where
, "payconf" =: pc , "payconf" =: pc
, "viewKey" =: vk , "viewKey" =: vk
, "crmToken" =: cT , "crmToken" =: cT
, "tips" =: oT
] ]
Nothing -> Nothing ->
Doc Doc
@ -273,24 +279,23 @@ instance Val Owner where
, "payconf" =: pc , "payconf" =: pc
, "viewKey" =: vk , "viewKey" =: vk
, "crmToken" =: cT , "crmToken" =: cT
, "tips" =: oT
] ]
-- | Type to represent informational data for Owners from UI -- | Type to represent informational data for Owners from UI
data OwnerData = data OwnerData = OwnerData
OwnerData { od_first :: T.Text
{ od_first :: T.Text , od_last :: T.Text
, od_last :: T.Text , od_name :: T.Text
, od_name :: T.Text , od_street :: T.Text
, od_street :: T.Text , od_city :: T.Text
, od_city :: T.Text , od_state :: T.Text
, od_state :: T.Text , od_postal :: T.Text
, od_postal :: T.Text , od_country :: T.Text
, od_country :: T.Text , od_email :: T.Text
, od_email :: T.Text , od_website :: T.Text
, od_website :: T.Text , od_phone :: T.Text
, od_phone :: T.Text } deriving (Eq, Show, Generic)
}
deriving (Eq, Show, Generic)
instance FromJSON OwnerData where instance FromJSON OwnerData where
parseJSON = parseJSON =
@ -308,25 +313,24 @@ instance FromJSON OwnerData where
ph <- obj .: "phone" ph <- obj .: "phone"
pure $ OwnerData f l n s c st p co e w ph pure $ OwnerData f l n s c st p co e w ph
data OwnerSettings = data OwnerSettings = OwnerSettings
OwnerSettings { os_id :: Maybe ObjectId
{ os_id :: Maybe ObjectId , os_address :: T.Text
, os_address :: T.Text , os_name :: T.Text
, os_name :: T.Text , os_currency :: T.Text
, os_currency :: T.Text , os_tax :: Bool
, os_tax :: Bool , os_taxValue :: Double
, os_taxValue :: Double , os_vat :: Bool
, os_vat :: Bool , os_vatValue :: Double
, os_vatValue :: Double , os_paid :: Bool
, os_paid :: Bool , os_zats :: Bool
, os_zats :: Bool , os_invoices :: Bool
, os_invoices :: Bool , os_expiration :: UTCTime
, os_expiration :: UTCTime , os_payconf :: Bool
, os_payconf :: Bool , os_crmToken :: T.Text
, os_crmToken :: T.Text , os_viewKey :: T.Text
, os_viewKey :: T.Text , os_tips :: Bool
} } deriving (Eq, Show, Generic)
deriving (Eq, Show, Generic)
instance FromJSON OwnerSettings where instance FromJSON OwnerSettings where
parseJSON = parseJSON =
@ -346,11 +350,28 @@ instance FromJSON OwnerSettings where
pc <- obj .: "payconf" pc <- obj .: "payconf"
cT <- obj .: "crmToken" cT <- obj .: "crmToken"
vK <- obj .: "viewkey" vK <- obj .: "viewkey"
oT <- obj .: "tips"
pure $ pure $
OwnerSettings ((Just . read) =<< i) a n c t tV v vV p z inv e pc cT vK OwnerSettings
((Just . read) =<< i)
a
n
c
t
tV
v
vV
p
z
inv
e
pc
cT
vK
oT
instance ToJSON OwnerSettings where instance ToJSON OwnerSettings where
toJSON (OwnerSettings i a n c t tV v vV p z inv e pc cT vK) = toJSON (OwnerSettings i a n c t tV v vV p z inv e pc cT vK oT) =
object object
[ "_id" .= maybe "" show i [ "_id" .= maybe "" show i
, "address" .= a , "address" .= a
@ -367,6 +388,7 @@ instance ToJSON OwnerSettings where
, "payconf" .= pc , "payconf" .= pc
, "crmToken" .= cT , "crmToken" .= cT
, "viewkey" .= keyObfuscate vK , "viewkey" .= keyObfuscate vK
, "tips" .= oT
] ]
where where
keyObfuscate s keyObfuscate s
@ -392,6 +414,7 @@ getOwnerSettings o =
(opayconf o) (opayconf o)
(ocrmToken o) (ocrmToken o)
(oviewkey o) (oviewkey o)
(otips o)
-- Database actions -- Database actions
-- | Function to upsert an Owner -- | Function to upsert an Owner
@ -424,6 +447,10 @@ findExpiringOwners now =
["paid" =: True, "expiration" =: ["$lte" =: addUTCTime 172800 now]] ["paid" =: True, "expiration" =: ["$lte" =: addUTCTime 172800 now]]
"owners") "owners")
findWithKeys :: Action IO [Document]
findWithKeys =
rest =<< find (select ["paid" =: True, "payconf" =: True] "owners")
removePro :: T.Text -> Action IO () removePro :: T.Text -> Action IO ()
removePro o = removePro o =
modify (select ["address" =: o] "owners") ["$set" =: ["invoices" =: False]] modify (select ["address" =: o] "owners") ["$set" =: ["invoices" =: False]]
@ -442,6 +469,7 @@ updateOwnerSettings os =
, "zats" =: os_zats os , "zats" =: os_zats os
, "payconf" =: os_payconf os , "payconf" =: os_payconf os
, "crmToken" =: os_crmToken os , "crmToken" =: os_crmToken os
, "tips" =: os_tips os
] ]
] ]
@ -450,14 +478,12 @@ upsertViewingKey o vk =
modify (select ["_id" =: o_id o] "owners") ["$set" =: ["viewKey" =: vk]] modify (select ["_id" =: o_id o] "owners") ["$set" =: ["viewKey" =: vk]]
-- | Type for a pro session -- | Type for a pro session
data ZGoProSession = data ZGoProSession = ZGoProSession
ZGoProSession { ps_id :: Maybe ObjectId
{ ps_id :: Maybe ObjectId , psaddress :: T.Text
, psaddress :: T.Text , psexpiration :: UTCTime
, psexpiration :: UTCTime , psclosed :: Bool
, psclosed :: Bool } deriving (Eq, Show)
}
deriving (Eq, Show)
instance Val ZGoProSession where instance Val ZGoProSession where
cast' (Doc d) = do cast' (Doc d) = do

View file

@ -15,6 +15,7 @@ import Control.Monad.IO.Class
import Crypto.RNG (newCryptoRNGState, runCryptoRNGT) import Crypto.RNG (newCryptoRNGState, runCryptoRNGT)
import Crypto.RNG.Utils (randomString) import Crypto.RNG.Utils (randomString)
import Data.Aeson import Data.Aeson
import Data.Aeson (decodeFileStrict)
import Data.Array import Data.Array
import qualified Data.Bson as B import qualified Data.Bson as B
import qualified Data.ByteArray as BA import qualified Data.ByteArray as BA
@ -25,7 +26,7 @@ import Data.Char
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.HexString import Data.HexString
import Data.Maybe import Data.Maybe
import qualified Data.Scientific as Scientific import qualified Data.Scientific as SC
import Data.SecureMem import Data.SecureMem
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as E import qualified Data.Text.Encoding as E
@ -37,9 +38,8 @@ import Data.Time.Format
import Data.Typeable import Data.Typeable
import qualified Data.UUID as U import qualified Data.UUID as U
import qualified Data.Vector as V import qualified Data.Vector as V
import Data.Vector.Internal.Check (doChecks)
import Data.Word import Data.Word
import Database.MongoDB hiding (Order) import Database.MongoDB hiding (Order, lookup)
import Debug.Trace import Debug.Trace
import GHC.Generics import GHC.Generics
import Item import Item
@ -53,6 +53,7 @@ import Numeric
import Order import Order
import Owner import Owner
import Payment import Payment
import System.IO
import System.IO.Unsafe import System.IO.Unsafe
import System.Random import System.Random
import Test.QuickCheck import Test.QuickCheck
@ -66,88 +67,50 @@ import Web.Scotty
import WooCommerce import WooCommerce
import Xero import Xero
import ZGoTx import ZGoTx
import ZcashHaskell.Orchard
import ZcashHaskell.Sapling import ZcashHaskell.Sapling
import ZcashHaskell.Types (RawData(..)) import ZcashHaskell.Types
import ZcashHaskell.Utils (decodeBech32) ( BlockResponse(..)
, DecodedNote(..)
, RawData(..)
, RawTxResponse(..)
, RpcCall(..)
, RpcError(..)
, RpcResponse(..)
, UnifiedFullViewingKey(..)
)
import ZcashHaskell.Utils (decodeBech32, makeZcashCall)
-- Models for API objects -- Models for API objects
-- | A type to model Zcash RPC calls data Payload r = Payload
data RpcCall = { payload :: r
RpcCall } deriving (Show, Generic, ToJSON)
{ jsonrpc :: T.Text
, callId :: T.Text
, method :: T.Text
, parameters :: [Data.Aeson.Value]
}
deriving (Show, Generic)
instance ToJSON RpcCall where
toJSON (RpcCall j c m p) =
object ["jsonrpc" .= j, "id" .= c, "method" .= m, "params" .= p]
-- | A type to model the response of the Zcash RPC
data RpcResponse r =
MakeRpcResponse
{ err :: Maybe RpcError
, respId :: T.Text
, result :: Maybe r
}
deriving (Show, Generic, ToJSON)
instance (FromJSON r) => FromJSON (RpcResponse r) where
parseJSON (Object obj) =
MakeRpcResponse <$> obj .: "error" <*> obj .: "id" <*> obj .: "result"
parseJSON _ = mzero
data RpcError =
RpcError
{ ecode :: Double
, emessage :: T.Text
}
deriving (Show, Generic, ToJSON)
instance FromJSON RpcError where
parseJSON =
withObject "RpcError" $ \obj -> do
c <- obj .: "code"
m <- obj .: "message"
pure $ RpcError c m
data Payload r =
Payload
{ payload :: r
}
deriving (Show, Generic, ToJSON)
instance (FromJSON r) => FromJSON (Payload r) where instance (FromJSON r) => FromJSON (Payload r) where
parseJSON (Object obj) = Payload <$> obj .: "payload" parseJSON (Object obj) = Payload <$> obj .: "payload"
parseJSON _ = mzero parseJSON _ = mzero
-- | Type to model a (simplified) block of Zcash blockchain -- | Type to model a (simplified) block of Zcash blockchain
data Block = data Block = Block
Block { height :: Integer
{ height :: Integer , size :: Integer
, size :: Integer } deriving (Show, Generic, ToJSON)
}
deriving (Show, Generic, ToJSON)
instance FromJSON Block where instance FromJSON Block where
parseJSON (Object obj) = Block <$> obj .: "height" <*> obj .: "size" parseJSON (Object obj) = Block <$> obj .: "height" <*> obj .: "size"
parseJSON _ = mzero parseJSON _ = mzero
-- | Type to model a Zcash shielded transaction -- | Type to model a Zcash shielded transaction
data ZcashTx = data ZcashTx = ZcashTx
ZcashTx { ztxid :: T.Text
{ ztxid :: T.Text , zamount :: Double
, zamount :: Double , zamountZat :: Integer
, zamountZat :: Integer , zblockheight :: Integer
, zblockheight :: Integer , zblocktime :: Integer
, zblocktime :: Integer , zchange :: Bool
, zchange :: Bool , zconfirmations :: Integer
, zconfirmations :: Integer , zmemo :: T.Text
, zmemo :: T.Text } deriving (Show, Generic)
}
deriving (Show, Generic)
instance FromJSON ZcashTx where instance FromJSON ZcashTx where
parseJSON = parseJSON =
@ -196,14 +159,12 @@ instance Arbitrary ZcashTx where
ZcashTx a aZ t bh bt c cm <$> arbitrary ZcashTx a aZ t bh bt c cm <$> arbitrary
-- | A type to model an address group -- | A type to model an address group
data AddressGroup = data AddressGroup = AddressGroup
AddressGroup { agsource :: AddressSource
{ agsource :: AddressSource , agtransparent :: [ZcashAddress]
, agtransparent :: [ZcashAddress] , agsapling :: [ZcashAddress]
, agsapling :: [ZcashAddress] , agunified :: [ZcashAddress]
, agunified :: [ZcashAddress] } deriving (Show, Generic)
}
deriving (Show, Generic)
instance FromJSON AddressGroup where instance FromJSON AddressGroup where
parseJSON = parseJSON =
@ -284,14 +245,12 @@ instance FromJSON ZcashPool where
"orchard" -> return Orchard "orchard" -> return Orchard
_ -> fail "Not a known Zcash pool" _ -> fail "Not a known Zcash pool"
data ZcashAddress = data ZcashAddress = ZcashAddress
ZcashAddress { source :: AddressSource
{ source :: AddressSource , pool :: [ZcashPool]
, pool :: [ZcashPool] , account :: Maybe Integer
, account :: Maybe Integer , addy :: T.Text
, addy :: T.Text } deriving (Eq)
}
deriving (Eq)
instance Show ZcashAddress where instance Show ZcashAddress where
show (ZcashAddress s p i a) = show (ZcashAddress s p i a) =
@ -315,13 +274,11 @@ encodeHexText t = T.unpack . toText . fromBytes $ E.encodeUtf8 t
-- Types for the ZGo database documents -- Types for the ZGo database documents
-- | Type to model a country for the database's country list -- | Type to model a country for the database's country list
data Country = data Country = Country
Country { _id :: String
{ _id :: String , name :: T.Text
, name :: T.Text , code :: T.Text
, code :: T.Text } deriving (Eq, Show, Generic, ToJSON)
}
deriving (Eq, Show, Generic, ToJSON)
parseCountryBson :: B.Document -> Maybe Country parseCountryBson :: B.Document -> Maybe Country
parseCountryBson d = do parseCountryBson d = do
@ -364,10 +321,11 @@ zToZGoTx (ZcashTx t a aZ bh bt c conf m) = do
zToZGoTx' :: Config -> Pipe -> ZcashTx -> IO () zToZGoTx' :: Config -> Pipe -> ZcashTx -> IO ()
zToZGoTx' config pipe (ZcashTx t a aZ bh bt c conf m) = do zToZGoTx' config pipe (ZcashTx t a aZ bh bt c conf m) = do
when (conf < 100) $ do when (conf < c_confirmations config) $ do
let zM = runParser pZGoMemo (T.unpack t) m let zM = runParser pZGoMemo (T.unpack t) m
case zM of case zM of
Right zM' -> do Right zM' -> do
print zM'
let tx = let tx =
ZGoTx ZGoTx
Nothing Nothing
@ -384,14 +342,12 @@ zToZGoTx' config pipe (ZcashTx t a aZ bh bt c conf m) = do
Left e -> print $ "Failed to parse ZGo memo: " ++ show e Left e -> print $ "Failed to parse ZGo memo: " ++ show e
-- |Type to model a price in the ZGo database -- |Type to model a price in the ZGo database
data ZGoPrice = data ZGoPrice = ZGoPrice
ZGoPrice { _id :: String
{ _id :: String , currency :: T.Text
, currency :: T.Text , price :: Double
, price :: Double , timestamp :: UTCTime
, timestamp :: UTCTime } deriving (Eq, Show, Generic, ToJSON)
}
deriving (Eq, Show, Generic, ToJSON)
parseZGoPrice :: B.Document -> Maybe ZGoPrice parseZGoPrice :: B.Document -> Maybe ZGoPrice
parseZGoPrice d = do parseZGoPrice d = do
@ -418,12 +374,7 @@ listCountries :: Action IO [Document]
listCountries = rest =<< find (select [] "countries") listCountries = rest =<< find (select [] "countries")
sendPin :: sendPin ::
BS.ByteString BS.ByteString -> BS.ByteString -> T.Text -> T.Text -> T.Text -> IO String
-> BS.ByteString
-> T.Text
-> T.Text
-> T.Text
-> Action IO String
sendPin nodeUser nodePwd nodeAddress addr pin = do sendPin nodeUser nodePwd nodeAddress addr pin = do
let pd = let pd =
[ Data.Aeson.String nodeAddress [ Data.Aeson.String nodeAddress
@ -435,17 +386,73 @@ sendPin nodeUser nodePwd nodeAddress addr pin = do
, "memo" .= encodeHexText ("ZGo PIN: " <> pin) , "memo" .= encodeHexText ("ZGo PIN: " <> pin)
] ]
]) ])
, Data.Aeson.Number $ SC.scientific 1 1
, Data.Aeson.Null
, Data.Aeson.String "AllowRevealedAmounts"
] ]
r <- liftIO $ try $ makeZcashCall nodeUser nodePwd "z_sendmany" pd -- IO (Either HttpException (Response Object)) r <- liftIO $ try $ makeZcashCall nodeUser nodePwd "z_sendmany" pd
case r of case r of
Right res -> do Right res -> do
let sCode = getResponseStatus (res :: Response Object) let sCode = getResponseStatus (res :: Response (RpcResponse T.Text))
let rBody = getResponseBody res
if sCode == ok200 if sCode == ok200
then return "Pin sent!" then do
case result rBody of
Nothing -> return "Couldn't parse node response"
Just x -> do
putStr " Sending."
checkOpResult nodeUser nodePwd x
return "Pin sent!"
else return "Pin sending failed :(" else return "Pin sending failed :("
Left ex -> Left ex ->
return $ "Failed to send tx to node :(" ++ show (ex :: HttpException) return $ "Failed to send tx to node :(" ++ show (ex :: HttpException)
-- | Type for Operation Result
data OpResult = OpResult
{ opsuccess :: T.Text
, opmessage :: Maybe T.Text
, optxid :: Maybe T.Text
} deriving (Show, Eq)
instance FromJSON OpResult where
parseJSON =
withObject "OpResult" $ \obj -> do
s <- obj .: "status"
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
checkOpResult :: BS.ByteString -> BS.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 = getResponseBody response :: (RpcResponse [OpResult])
case result rpcResp of
Nothing -> putStrLn "Couldn't read response from node"
Just opCode -> mapM_ showResult opCode
where
showResult t =
case opsuccess t of
"success" ->
putStrLn $ " Success! Tx ID: " ++ maybe "" T.unpack (optxid t)
"executing" -> do
putStr "."
hFlush stdout
threadDelay 1000000 >> checkOpResult user pwd opid
_ -> putStrLn $ " Failed :( " ++ maybe "" T.unpack (opmessage t)
-- | Function to create user from ZGoTx -- | Function to create user from ZGoTx
addUser :: addUser ::
BS.ByteString BS.ByteString
@ -460,7 +467,7 @@ addUser nodeUser nodePwd p db node (Just tx) = do
isNew <- liftIO $ isUserNew p db tx isNew <- liftIO $ isUserNew p db tx
when isNew $ do when isNew $ do
newPin <- liftIO generatePin newPin <- liftIO generatePin
_ <- sendPin nodeUser nodePwd node (address tx) (T.pack newPin) _ <- liftIO $ sendPin nodeUser nodePwd node (address tx) (T.pack newPin)
let pinHash = let pinHash =
BLK.hash BLK.hash
[ BA.pack . BS.unpack . C.pack . T.unpack $ [ BA.pack . BS.unpack . C.pack . T.unpack $
@ -584,6 +591,7 @@ routes pipe config = do
let nodeUser = c_nodeUser config let nodeUser = c_nodeUser config
let nodePwd = c_nodePwd config let nodePwd = c_nodePwd config
let nodeAddress = c_nodeAddress config let nodeAddress = c_nodeAddress config
let dbName = c_dbName config
middleware $ middleware $
cors $ cors $
const $ const $
@ -717,9 +725,11 @@ routes pipe config = do
[ "reportType" .= [ "reportType" .=
(7 :: Integer) (7 :: Integer)
, "order" .= , "order" .=
(Nothing :: Maybe ZGoOrder) (Nothing :: Maybe
ZGoOrder)
, "shop" .= , "shop" .=
(Nothing :: Maybe String) (Nothing :: Maybe
String)
]) ])
Just cp -> do Just cp -> do
let newOrder = let newOrder =
@ -752,10 +762,13 @@ routes pipe config = do
(xr_shortCode (xr_shortCode
invReq) invReq)
(T.pack tk) (T.pack tk)
0
0
0
_ <- _ <-
liftAndCatchIO $ liftAndCatchIO $
run $ run $
upsertOrder newOrder upsertOrder newOrder 0 0
finalOrder <- finalOrder <-
liftAndCatchIO $ liftAndCatchIO $
run $ run $
@ -789,7 +802,8 @@ routes pipe config = do
[ "reportType" .= [ "reportType" .=
(8 :: Integer) (8 :: Integer)
, "order" .= , "order" .=
(Nothing :: Maybe ZGoOrder) (Nothing :: Maybe
ZGoOrder)
, "shop" .= , "shop" .=
(Nothing :: Maybe String) (Nothing :: Maybe String)
]) ])
@ -959,7 +973,8 @@ routes pipe config = do
where blk3Hash :: String -> String where blk3Hash :: String -> String
blk3Hash s = blk3Hash s =
show show
(BLK.hash [BA.pack . BS.unpack . C.pack $ s :: BA.Bytes] :: BLK.Digest BLK.DEFAULT_DIGEST_LEN) (BLK.hash [BA.pack . BS.unpack . C.pack $ s :: BA.Bytes] :: BLK.Digest
BLK.DEFAULT_DIGEST_LEN)
get "/woopayment" $ do get "/woopayment" $ do
oid <- param "ownerid" oid <- param "ownerid"
t <- param "token" t <- param "token"
@ -1029,6 +1044,9 @@ routes pipe config = do
[T.pack sUrl, "-", ordId, "-", orderKey]) [T.pack sUrl, "-", ordId, "-", orderKey])
"" ""
(T.pack tk) (T.pack tk)
0
0
0
newId <- liftAndCatchIO $ run (insertWooOrder newOrder) newId <- liftAndCatchIO $ run (insertWooOrder newOrder)
status ok200 status ok200
Web.Scotty.json Web.Scotty.json
@ -1190,6 +1208,7 @@ routes pipe config = do
False False
"" ""
"" ""
False
status accepted202 status accepted202
post "/api/ownersettings" $ do post "/api/ownersettings" $ do
s <- param "session" s <- param "session"
@ -1218,37 +1237,56 @@ routes pipe config = do
case cast' . Doc =<< u of case cast' . Doc =<< u of
Nothing -> status unauthorized401 Nothing -> status unauthorized401
Just u' -> do Just u' -> do
if isValidSaplingViewingKey qBytes if isValidSaplingViewingKey $ C.pack q
then if matchSaplingAddress then do
qBytes if matchSaplingAddress
(bytes . decodeBech32 . C.pack . T.unpack $ uaddress u') qBytes
then do (bytes . decodeBech32 . C.pack . T.unpack $ uaddress u')
owner <- liftAndCatchIO $ run (findOwner $ uaddress u') then do
case cast' . Doc =<< owner of owner <- liftAndCatchIO $ run (findOwner $ uaddress u')
Nothing -> status badRequest400 case cast' . Doc =<< owner of
Just o' -> do Nothing -> status badRequest400
unless (oviewkey o' /= "") $ do Just o' -> do
vkInfo <- unless (oviewkey o' /= "") $ do
liftAndCatchIO $ liftAndCatchIO $ run (upsertViewingKey o' q)
makeZcashCall status created201
nodeUser else status forbidden403
nodePwd else case decodeUfvk (C.pack q) of
"z_importviewingkey" Nothing -> status badRequest400
[ Data.Aeson.String (T.strip . T.pack $ q) Just fvk -> do
, "no" if isValidUnifiedAddress $
] C.pack . T.unpack $ uaddress u'
let content = then do
getResponseBody vkInfo :: RpcResponse Object if matchOrchardAddress
if isNothing (err content) (C.pack q)
then do (C.pack . T.unpack $ uaddress u')
_ <- then do
liftAndCatchIO $ run (upsertViewingKey o' q) owner <-
status created201 liftAndCatchIO $ run (findOwner $ uaddress u')
else do case cast' . Doc =<< owner of
text $ L.pack . show $ err content Nothing -> status badRequest400
status badRequest400 Just o' -> do
else status forbidden403 unless (oviewkey o' /= "") $ do
else status badRequest400 liftAndCatchIO $
run (upsertViewingKey o' q)
status created201
else status forbidden403
else do
if matchSaplingAddress
(s_key fvk)
(bytes . decodeBech32 . C.pack . T.unpack $
uaddress u')
then do
owner <-
liftAndCatchIO $ run (findOwner $ uaddress u')
case cast' . Doc =<< owner of
Nothing -> status badRequest400
Just o' -> do
unless (oviewkey o' /= "") $ do
liftAndCatchIO $
run (upsertViewingKey o' q)
status created201
else status forbidden403
--Get items associated with the given address --Get items associated with the given address
get "/api/items" $ do get "/api/items" $ do
session <- param "session" session <- param "session"
@ -1302,15 +1340,12 @@ routes pipe config = do
get "/price" $ do get "/price" $ do
curr <- param "currency" curr <- param "currency"
pr <- liftAndCatchIO $ run (findPrice curr) pr <- liftAndCatchIO $ run (findPrice curr)
case pr of case parseZGoPrice =<< pr of
Nothing -> do Nothing -> do
status noContent204 status noContent204
Just p -> do Just p -> do
Web.Scotty.json Web.Scotty.json
(object (object ["message" .= ("Price found!" :: String), "price" .= toJSON p])
[ "message" .= ("Price found!" :: String)
, "price" .= toJSON (parseZGoPrice p)
])
--Get all closed orders for the address --Get all closed orders for the address
get "/api/allorders" $ do get "/api/allorders" $ do
session <- param "session" session <- param "session"
@ -1373,26 +1408,26 @@ routes pipe config = do
, "order" .= toJSON (pOrder :: ZGoOrder) , "order" .= toJSON (pOrder :: ZGoOrder)
]) ])
--Upsert xero order --Upsert xero order
post "/api/orderx" $ do {-post "/api/orderx" $ do-}
newOrder <- jsonData {-newOrder <- jsonData-}
let q = payload (newOrder :: Payload ZGoOrder) {-let q = payload (newOrder :: Payload ZGoOrder)-}
_ <- liftIO $ run (upsertXeroOrder q) {-_ <- liftIO $ run (upsertXeroOrder q)-}
myOrder <- {-myOrder <--}
liftAndCatchIO $ {-liftAndCatchIO $-}
run (findXeroOrder (qaddress q) (qexternalInvoice q) (qshortCode q)) {-run (findXeroOrder (qaddress q) (qexternalInvoice q) (qshortCode q))-}
case myOrder of {-case myOrder of-}
Nothing -> status noContent204 {-Nothing -> status noContent204-}
Just o -> do {-Just o -> do-}
let o' = cast' (Doc o) {-let o' = cast' (Doc o)-}
case o' of {-case o' of-}
Nothing -> status internalServerError500 {-Nothing -> status internalServerError500-}
Just pOrder -> do {-Just pOrder -> do-}
status created201 {-status created201-}
Web.Scotty.json {-Web.Scotty.json-}
(object {-(object-}
[ "message" .= ("Order found!" :: String) {-[ "message" .= ("Order found!" :: String)-}
, "order" .= toJSON (pOrder :: ZGoOrder) {-, "order" .= toJSON (pOrder :: ZGoOrder)-}
]) {-])-}
-- Upsert order -- Upsert order
post "/api/order" $ do post "/api/order" $ do
newOrder <- jsonData newOrder <- jsonData
@ -1402,20 +1437,73 @@ routes pipe config = do
case cast' . Doc =<< user of case cast' . Doc =<< user of
Nothing -> status unauthorized401 Nothing -> status unauthorized401
Just u -> do Just u -> do
if uaddress u == qaddress q owner <- liftAndCatchIO $ run $ findOwner (uaddress u)
then do case cast' . Doc =<< owner of
if qtoken q == "" Nothing -> status badRequest400
then do Just o -> do
t <- liftIO generateToken let taxRate =
_ <- if otax o
liftAndCatchIO $ then otaxValue o
run (upsertOrder $ setOrderToken (T.pack t) q) else 0
status created201 let vatRate =
else do if ovat o
_ <- liftAndCatchIO $ run (upsertOrder q) then ovatValue o
status created201 else 0
else status forbidden403 dbOrder <-
--Delete order liftAndCatchIO $ run (findOrderById $ maybe "0" show (q_id q))
case cast' . Doc =<< dbOrder of
Nothing -> do
if uaddress u == qaddress q
then do
if qtoken q == ""
then do
t <- liftIO generateToken
_ <-
liftAndCatchIO $
run
(upsertOrder
(setOrderToken (T.pack t) q)
taxRate
vatRate)
status created201
else do
_ <-
liftAndCatchIO $
access
pipe
master
dbName
(upsertOrder q taxRate vatRate)
status created201
else status forbidden403
Just dbO -> do
if qaddress q == qaddress dbO && qsession q == qsession dbO
then do
if uaddress u == qaddress q
then do
if qtoken q == ""
then do
t <- liftIO generateToken
_ <-
liftAndCatchIO $
run
(upsertOrder
(setOrderToken (T.pack t) q)
taxRate
vatRate)
status created201
else do
_ <-
liftAndCatchIO $
access
pipe
master
dbName
(upsertOrder q taxRate vatRate)
status created201
else status forbidden403
else status forbidden403
--Delete order
Web.Scotty.delete "/api/order/:id" $ do Web.Scotty.delete "/api/order/:id" $ do
oId <- param "id" oId <- param "id"
session <- param "session" session <- param "session"
@ -1485,25 +1573,24 @@ routes pipe config = do
{-liftAndCatchIO $-} {-liftAndCatchIO $-}
{-mapM (run . loadLangComponent) (langComp :: [LangComponent])-} {-mapM (run . loadLangComponent) (langComp :: [LangComponent])-}
{-status created201-} {-status created201-}
{-(MonadIO m, FromJSON a)-}
{-=> BS.ByteString-}
{--> BS.ByteString-}
{--> T.Text-}
{--> [Data.Aeson.Value]-}
{--> m (Response a)-}
{-let payload =-}
{-RpcCall {jsonrpc = "1.0", callId = "test", method = m, parameters = p}-}
{-let myRequest =-}
{-setRequestBodyJSON payload $-}
{-setRequestPort 8232 $-}
{-setRequestBasicAuth username password $-}
{-setRequestMethod "POST" defaultRequest-}
{-httpJSON myRequest-}
-- | Make a Zcash RPC call -- | Make a Zcash RPC call
makeZcashCall :: {-makeZcashCall ::-}
(MonadIO m, FromJSON a) {-makeZcashCall username password m p = do-}
=> BS.ByteString
-> BS.ByteString
-> T.Text
-> [Data.Aeson.Value]
-> m (Response a)
makeZcashCall username password m p = do
let payload =
RpcCall {jsonrpc = "1.0", callId = "test", method = m, parameters = p}
let myRequest =
setRequestBodyJSON payload $
setRequestPort 8232 $
setRequestBasicAuth username password $
setRequestMethod "POST" defaultRequest
httpJSON myRequest
-- |Timer for repeating actions -- |Timer for repeating actions
setInterval :: Int -> IO () -> IO () setInterval :: Int -> IO () -> IO ()
setInterval secs func = do setInterval secs func = do
@ -1545,7 +1632,8 @@ listTxs user pwd a confs = do
user user
pwd pwd
"z_listreceivedbyaddress" "z_listreceivedbyaddress"
[Data.Aeson.String a, Data.Aeson.Number $ Scientific.scientific confs 0] :: IO (Either HttpException (Response (RpcResponse [ZcashTx]))) [Data.Aeson.String a, Data.Aeson.Number $ SC.scientific confs 0] :: IO
(Either HttpException (Response (RpcResponse [ZcashTx])))
case res of case res of
Right txList -> do Right txList -> do
let content = getResponseBody txList :: RpcResponse [ZcashTx] let content = getResponseBody txList :: RpcResponse [ZcashTx]
@ -1678,7 +1766,8 @@ scanPayments config pipe = do
listAddresses :: BS.ByteString -> BS.ByteString -> IO [ZcashAddress] listAddresses :: BS.ByteString -> BS.ByteString -> IO [ZcashAddress]
listAddresses user pwd = do listAddresses user pwd = do
response <- response <-
try $ makeZcashCall user pwd "listaddresses" [] :: IO (Either HttpException (Response (RpcResponse [AddressGroup]))) try $ makeZcashCall user pwd "listaddresses" [] :: IO
(Either HttpException (Response (RpcResponse [AddressGroup])))
case response of case response of
Right addrList -> do Right addrList -> do
let rpcResp = getResponseBody addrList let rpcResp = getResponseBody addrList
@ -1741,7 +1830,7 @@ payOwner p d x =
markOwnerPaid :: Pipe -> T.Text -> Payment -> IO () markOwnerPaid :: Pipe -> T.Text -> Payment -> IO ()
markOwnerPaid pipe db pmt = do markOwnerPaid pipe db pmt = do
user <- access pipe master db (findUser $ psession pmt) user <- access pipe master db (findUser $ psession pmt)
print pmt -- print pmt
let parsedUser = parseUserBson =<< user let parsedUser = parseUserBson =<< user
let zaddy = maybe "" uaddress parsedUser let zaddy = maybe "" uaddress parsedUser
owner <- access pipe master db $ findOwner zaddy owner <- access pipe master db $ findOwner zaddy
@ -1847,4 +1936,263 @@ generateToken = do
rngState <- newCryptoRNGState rngState <- newCryptoRNGState
runCryptoRNGT rngState $ randomString 24 "abcdef0123456789" runCryptoRNGT rngState $ randomString 24 "abcdef0123456789"
getBlockInfo ::
BS.ByteString -> BS.ByteString -> T.Text -> IO (Maybe BlockResponse)
getBlockInfo nodeUser nodePwd bh = do
blockInfo <-
makeZcashCall
nodeUser
nodePwd
"getblock"
[Data.Aeson.String bh, Number $ SC.scientific 1 0]
let content = getResponseBody blockInfo :: RpcResponse BlockResponse
if isNothing (err content)
then return $ result content
else do
print $ err content
return Nothing
scanTxNative :: Config -> Pipe -> IO ()
scanTxNative config pipe = do
let db = c_dbName config
keyOwnerList <- access pipe master db findWithKeys
unless (null keyOwnerList) $ do
let nodeUser = c_nodeUser config
let nodePwd = c_nodePwd config
let ownerList = mapMaybe (cast' . Doc) keyOwnerList
lastBlockData <- access pipe master db findBlock
latestBlock <- getBlockInfo nodeUser nodePwd "-1"
case latestBlock of
Nothing -> fail "No block data from node"
Just lB -> do
case cast' . Doc =<< lastBlockData of
Nothing -> do
print "Getting blocks"
blockList <-
mapM
(getBlockInfo nodeUser nodePwd . T.pack . show)
[(bl_height lB - 50) .. (bl_height lB)]
print "filtering blocks..."
let filteredBlockList = filter filterBlock blockList
print "extracting txs from blocks..."
let txIdList = concatMap extractTxs filteredBlockList
print "getting tx data from node..."
txList <- mapM (getTxData nodeUser nodePwd) txIdList
print "filtering txs..."
let filteredTxList = map fromJust $ filter filterTx txList
print "checking txs against keys..."
mapM_ (checkTx filteredTxList) ownerList
access pipe master (c_dbName config) $
upsertBlock (last $ catMaybes filteredBlockList)
Just lastBlock -> do
blockList' <-
mapM
(getBlockInfo nodeUser nodePwd . T.pack . show)
[(bl_height lastBlock + 1) .. (bl_height lB)]
print "filtering blocks..."
let filteredBlockList = filter filterBlock blockList'
print "extracting txs from blocks..."
let txIdList = concatMap extractTxs filteredBlockList
print "getting tx data from node..."
txList <- mapM (getTxData nodeUser nodePwd) txIdList
print "filtering txs..."
let filteredTxList = map fromJust $ filter filterTx txList
print "checking txs against keys..."
mapM_ (checkTx filteredTxList) ownerList
access pipe master (c_dbName config) $
upsertBlock (last $ catMaybes filteredBlockList)
where
filterBlock :: Maybe BlockResponse -> Bool
filterBlock b = maybe 0 bl_confirmations b >= 5
filterTx :: Maybe RawTxResponse -> Bool
filterTx t =
not (null (maybe [] rt_shieldedOutputs t)) ||
not (null (maybe [] rt_orchardActions t))
extractTxs :: Maybe BlockResponse -> [T.Text]
extractTxs = maybe [] bl_txs
getTxData ::
BS.ByteString -> BS.ByteString -> T.Text -> IO (Maybe RawTxResponse)
getTxData nodeUser nodePwd txid = do
txInfo <-
makeZcashCall
nodeUser
nodePwd
"getrawtransaction"
[Data.Aeson.String txid, Number $ SC.scientific 1 0]
let content = getResponseBody txInfo :: RpcResponse RawTxResponse
if isNothing (err content)
then return $ result content
else do
print $ err content
return Nothing
checkTx :: [RawTxResponse] -> Owner -> IO ()
checkTx txList' k = do
let sOutList = concatMap rt_shieldedOutputs txList'
if isValidSaplingViewingKey (E.encodeUtf8 $ oviewkey k)
then do
print "decoding Sapling tx"
let decodedSapList' = concatMap (decodeSaplingTx $ oviewkey k) txList'
let zList = catMaybes decodedSapList'
mapM_ (recordPayment pipe (c_dbName config) (oaddress k)) zList
else do
let vk = decodeUfvk $ E.encodeUtf8 $ oviewkey k
case vk of
Nothing -> print "Not a valid key"
Just v -> do
let decodedSapList =
concatMap (decodeUnifiedSaplingTx (s_key v)) txList'
let zList' = catMaybes decodedSapList
mapM_ (recordPayment pipe (c_dbName config) (oaddress k)) zList'
let decodedOrchList = concatMap (decodeUnifiedOrchardTx v) txList'
let oList = catMaybes decodedOrchList
mapM_ (recordPayment pipe (c_dbName config) (oaddress k)) oList
decodeSaplingTx :: T.Text -> RawTxResponse -> [Maybe ZcashTx]
decodeSaplingTx k t =
map
(buildZcashTx t .
decodeSaplingOutput (bytes (decodeBech32 $ E.encodeUtf8 k)))
(rt_shieldedOutputs t)
decodeUnifiedSaplingTx :: BS.ByteString -> RawTxResponse -> [Maybe ZcashTx]
decodeUnifiedSaplingTx k t =
map (buildZcashTx t . decodeSaplingOutput k) (rt_shieldedOutputs t)
decodeUnifiedOrchardTx ::
UnifiedFullViewingKey -> RawTxResponse -> [Maybe ZcashTx]
decodeUnifiedOrchardTx k t =
map (buildZcashTx t . decryptOrchardAction k) (rt_orchardActions t)
buildZcashTx :: RawTxResponse -> Maybe DecodedNote -> Maybe ZcashTx
buildZcashTx t n =
case n of
Nothing -> Nothing
Just n ->
Just $
ZcashTx
(rt_id t)
(fromIntegral (a_value n) / 100000000)
(toInteger $ a_value n)
(rt_blockheight t)
(rt_blocktime t)
False
(rt_confirmations t)
(E.decodeUtf8Lenient $ a_memo n)
recordPayment :: Pipe -> T.Text -> T.Text -> ZcashTx -> IO ()
recordPayment p dbName z x = do
let zM = runParser pZGoMemo (T.unpack . ztxid $ x) (zmemo x)
case zM of
Right m -> do
case m_orderId m of
Nothing -> print "Not an order Tx"
Just orderId -> do
print orderId
o <- access p master dbName $ findOrderById (T.unpack orderId)
let xOrder = o >>= (cast' . Doc)
case xOrder of
Nothing -> error "Failed to retrieve order from database"
Just xO -> do
when
(not (qpaid xO) &&
qtotalZec xO == zamount x && z == qaddress xO) $ do
let sReg = mkRegex "(.*)-([a-fA-f0-9]{24})"
let sResult = matchAllText sReg (T.unpack $ qsession xO)
if not (null sResult)
then case fst $ head sResult ! 1 of
"Xero" -> do
xeroConfig <- access p master dbName findXero
let xC = xeroConfig >>= (cast' . Doc)
case xC of
Nothing -> error "Failed to read Xero config"
Just xConf -> do
requestXeroToken
p
dbName
xConf
""
(qaddress xO)
payXeroInvoice
p
dbName
(qexternalInvoice xO)
(qaddress xO)
(qtotal xO)
(qtotalZec xO)
liftIO $
access p master dbName $
markOrderPaid (T.unpack orderId, zamount x)
"WC" -> do
let wOwner = fst $ head sResult ! 2
wooT <-
access p master dbName $
findWooToken $ Just (read wOwner)
let wT = wooT >>= (cast' . Doc)
case wT of
Nothing ->
error "Failed to read WooCommerce token"
Just wt -> do
let iReg = mkRegex "(.*)-(.*)-.*"
let iResult =
matchAllText
iReg
(T.unpack $ qexternalInvoice xO)
if not (null iResult)
then do
let wUrl =
E.decodeUtf8With lenientDecode .
B64.decodeLenient . C.pack $
fst $ head iResult ! 1
let iNum = fst $ head iResult ! 2
payWooOrder
(T.unpack wUrl)
(C.pack iNum)
(C.pack $ maybe "" show (q_id xO))
(C.pack . T.unpack $ w_token wt)
(C.pack . show $ qprice xO)
(C.pack . show $ qtotalZec xO)
liftIO $
access p master dbName $
markOrderPaid
(T.unpack orderId, zamount x)
else error
"Couldn't parse externalInvoice for WooCommerce"
_ -> putStrLn "Not an integration order"
else liftIO $
access p master dbName $
markOrderPaid (T.unpack orderId, zamount x)
Left e -> print "Unable to parse order memo"
debug = flip trace debug = flip trace
instance Val BlockResponse where
cast' (Doc d) = do
c <- B.lookup "confirmations" d
h <- B.lookup "height" d
t <- B.lookup "time" d
txs <- B.lookup "tx" d
Just (BlockResponse c h t txs)
cast' _ = Nothing
val (BlockResponse c h t txs) =
Doc
[ "confirmations" =: c
, "height" =: h
, "time" =: t
, "tx" =: txs
, "network" =: ("mainnet" :: String)
]
upsertBlock :: BlockResponse -> Action IO ()
upsertBlock b = do
let block = val b
case block of
Doc d -> upsert (select ["network" =: ("mainnet" :: String)] "blocks") d
_ -> return ()
findBlock :: Action IO (Maybe Document)
findBlock = findOne (select ["network" =: ("mainnet" :: String)] "blocks")
loadTranslations :: Pipe -> Config -> IO ()
loadTranslations pipe config = do
itemList <- decodeFileStrict "zgolanguagedb.json"
case itemList of
Nothing -> print "Couldn't not parse JSON file"
Just langItems ->
mapM_
(access pipe master (c_dbName config) . loadLangComponent)
(langItems :: [LangComponent])

View file

@ -9,26 +9,27 @@ import qualified Data.Bson as B
import Data.Char import Data.Char
import Data.Maybe import Data.Maybe
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as E
import qualified Data.UUID as U import qualified Data.UUID as U
import Data.Void import Data.Void
import Database.MongoDB import Database.MongoDB
import GHC.Generics import GHC.Generics
import Text.Megaparsec hiding (State) import Text.Megaparsec hiding (State)
import Text.Megaparsec.Char import Text.Megaparsec.Char
import ZcashHaskell.Orchard
import ZcashHaskell.Sapling (isValidShieldedAddress)
-- | Type to model a ZGo transaction -- | Type to model a ZGo transaction
data ZGoTx = data ZGoTx = ZGoTx
ZGoTx { _id :: Maybe ObjectId
{ _id :: Maybe ObjectId , address :: T.Text
, address :: T.Text , session :: T.Text
, session :: T.Text , confirmations :: Integer
, confirmations :: Integer , blocktime :: Integer
, blocktime :: Integer , amount :: Double
, amount :: Double , txid :: T.Text
, txid :: T.Text , memo :: T.Text
, memo :: T.Text } deriving (Eq, Show, Generic)
}
deriving (Eq, Show, Generic)
parseZGoTxBson :: B.Document -> Maybe ZGoTx parseZGoTxBson :: B.Document -> Maybe ZGoTx
parseZGoTxBson d = do parseZGoTxBson d = do
@ -100,19 +101,19 @@ instance Val ZGoTx where
] ]
-- | Type to represent and parse ZGo memos -- | Type to represent and parse ZGo memos
data ZGoMemo = data ZGoMemo = ZGoMemo
ZGoMemo { m_session :: Maybe U.UUID
{ m_session :: Maybe U.UUID , m_address :: Maybe T.Text
, m_address :: Maybe T.Text , m_payment :: Bool
, m_payment :: Bool , m_orderId :: Maybe T.Text
} } deriving (Eq, Show)
deriving (Eq, Show)
data MemoToken data MemoToken
= Login !U.UUID = Login !U.UUID
| PayMsg !U.UUID | PayMsg !U.UUID
| Address !T.Text | Address !T.Text
| Msg !T.Text | Msg !T.Text
| OrderId !T.Text
deriving (Show, Eq) deriving (Show, Eq)
type Parser = Parsec Void T.Text type Parser = Parsec Void T.Text
@ -135,9 +136,23 @@ pSaplingAddress :: Parser MemoToken
pSaplingAddress = do pSaplingAddress = do
string "zs" string "zs"
a <- some alphaNumChar a <- some alphaNumChar
if length a /= 76 if isValidShieldedAddress (E.encodeUtf8 $ "zs" <> T.pack a)
then fail "Failed to parse Sapling address" then pure $ Address $ T.pack ("zs" <> a)
else pure $ Address $ T.pack ("zs" <> a) else fail "Failed to parse Sapling address"
pUnifiedAddress :: Parser MemoToken
pUnifiedAddress = do
string "u1"
a <- some alphaNumChar
if isValidUnifiedAddress (E.encodeUtf8 $ "u1" <> T.pack a)
then pure $ Address $ T.pack ("u1" <> a)
else fail "Failed to parse Unified Address"
pOrderId :: Parser MemoToken
pOrderId = do
string "ZGo Order::"
a <- some hexDigitChar
pure $ OrderId . T.pack $ a
pMsg :: Parser MemoToken pMsg :: Parser MemoToken
pMsg = do pMsg = do
@ -150,7 +165,7 @@ pMsg = do
pMemo :: Parser MemoToken pMemo :: Parser MemoToken
pMemo = do pMemo = do
optional $ some spaceChar optional $ some spaceChar
t <- pSession <|> pSaplingAddress <|> pMsg t <- pSession <|> pSaplingAddress <|> pUnifiedAddress <|> pOrderId <|> pMsg
optional $ some spaceChar optional $ some spaceChar
return t return t
@ -175,8 +190,15 @@ isMemoToken kind t =
pZGoMemo :: Parser ZGoMemo pZGoMemo :: Parser ZGoMemo
pZGoMemo = do pZGoMemo = do
tks <- some pMemo tks <- some pMemo
pure $ ZGoMemo (isSession tks) (isAddress tks) (isPayment tks) pure $ ZGoMemo (isSession tks) (isAddress tks) (isPayment tks) (isOrder tks)
where where
isOrder [] = Nothing
isOrder tks =
if not (null tks)
then case head tks of
OrderId x -> Just x
_ -> isOrder $ tail tks
else Nothing
isPayment [] = False isPayment [] = False
isPayment tks = isPayment tks =
not (null tks) && not (null tks) &&

View file

@ -17,7 +17,7 @@
# #
# resolver: ./custom-snapshot.yaml # resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-20.23 resolver: lts-21.17
#url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/4.yaml #url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/4.yaml
# User packages to be built. # User packages to be built.
@ -45,11 +45,14 @@ extra-deps:
- git: https://github.com/reach-sh/haskell-hexstring.git - git: https://github.com/reach-sh/haskell-hexstring.git
commit: 085c16fb21b9f856a435a3faab980e7e0b319341 commit: 085c16fb21b9f856a435a3faab980e7e0b319341
- git: https://git.vergara.tech/Vergara_Tech/zcash-haskell.git - git: https://git.vergara.tech/Vergara_Tech/zcash-haskell.git
commit: fef3d3af35a09db718cddb8fc9166b2d2691a744 commit: 1d558fc646a7758d60a721124812070de222c2e1
- git: https://git.vergara.tech/Vergara_Tech/haskell-foreign-rust.git - git: https://git.vergara.tech/Vergara_Tech/haskell-foreign-rust.git
commit: 787c2e813eb3a5d16c375d4b37dfefbd2adcdf05 commit: 787c2e813eb3a5d16c375d4b37dfefbd2adcdf05
- git: https://github.com/well-typed/borsh.git - git: https://github.com/well-typed/borsh.git
commit: d2fcfa159e0a844b1ec5e8ed3e232d4b380fa831 commit: d2fcfa159e0a844b1ec5e8ed3e232d4b380fa831
- git: https://git.vergara.tech/Vergara_Tech/mongodb.git
commit: 63bba3a6d30e5fd73c71fd7da752b2647d94f58e
# - network-2.8.0.1@sha256:a79f3cf88b2623d5f2e7a8fc7962055f6858d6beb6d13c2aef43c20a5060cf28,3034
- aeson-2.1.2.1@sha256:5b8d62a60963a925c4d123a46e42a8e235a32188522c9f119f64ac228c2612a7,6359 - aeson-2.1.2.1@sha256:5b8d62a60963a925c4d123a46e42a8e235a32188522c9f119f64ac228c2612a7,6359
- vector-0.13.0.0@sha256:fa5cac81a17a5af388716792e8b99c24b3b66770086756d0d8b23f8272a0244c,9112 - vector-0.13.0.0@sha256:fa5cac81a17a5af388716792e8b99c24b3b66770086756d0d8b23f8272a0244c,9112
- generically-0.1.1 - generically-0.1.1

View file

@ -16,15 +16,15 @@ packages:
commit: 085c16fb21b9f856a435a3faab980e7e0b319341 commit: 085c16fb21b9f856a435a3faab980e7e0b319341
git: https://github.com/reach-sh/haskell-hexstring.git git: https://github.com/reach-sh/haskell-hexstring.git
- completed: - completed:
commit: fef3d3af35a09db718cddb8fc9166b2d2691a744 commit: 1d558fc646a7758d60a721124812070de222c2e1
git: https://git.vergara.tech/Vergara_Tech/zcash-haskell.git git: https://git.vergara.tech/Vergara_Tech/zcash-haskell.git
name: zcash-haskell name: zcash-haskell
pantry-tree: pantry-tree:
sha256: ec7782cf2646da17548d59af0ea98dcbaac1b6c2176258c696a7f508db6dbc21 sha256: eab3c6817bb3cb5738725824d16eb023cb2967ef3bbaa8f8252524602f606dbb
size: 1126 size: 1229
version: 0.1.0 version: 0.2.0
original: original:
commit: fef3d3af35a09db718cddb8fc9166b2d2691a744 commit: 1d558fc646a7758d60a721124812070de222c2e1
git: https://git.vergara.tech/Vergara_Tech/zcash-haskell.git git: https://git.vergara.tech/Vergara_Tech/zcash-haskell.git
- completed: - completed:
commit: 787c2e813eb3a5d16c375d4b37dfefbd2adcdf05 commit: 787c2e813eb3a5d16c375d4b37dfefbd2adcdf05
@ -48,6 +48,17 @@ packages:
original: original:
commit: d2fcfa159e0a844b1ec5e8ed3e232d4b380fa831 commit: d2fcfa159e0a844b1ec5e8ed3e232d4b380fa831
git: https://github.com/well-typed/borsh.git git: https://github.com/well-typed/borsh.git
- completed:
commit: 63bba3a6d30e5fd73c71fd7da752b2647d94f58e
git: https://git.vergara.tech/Vergara_Tech/mongodb.git
name: mongoDB
pantry-tree:
sha256: 63af9dc2612131fb5d1ea9d75b7055d5d0b28ca443149be1fb47c22bf204128f
size: 2297
version: 2.7.1.2
original:
commit: 63bba3a6d30e5fd73c71fd7da752b2647d94f58e
git: https://git.vergara.tech/Vergara_Tech/mongodb.git
- completed: - completed:
hackage: aeson-2.1.2.1@sha256:5b8d62a60963a925c4d123a46e42a8e235a32188522c9f119f64ac228c2612a7,6359 hackage: aeson-2.1.2.1@sha256:5b8d62a60963a925c4d123a46e42a8e235a32188522c9f119f64ac228c2612a7,6359
pantry-tree: pantry-tree:
@ -92,7 +103,7 @@ packages:
hackage: crypto-rng-0.3.0.1@sha256:04f4ae75943ecad8b794950985054130f272d65a588b6b6528f56df0bfedc4dc,1565 hackage: crypto-rng-0.3.0.1@sha256:04f4ae75943ecad8b794950985054130f272d65a588b6b6528f56df0bfedc4dc,1565
snapshots: snapshots:
- completed: - completed:
sha256: 4c972e067bae16b95961dbfdd12e07f1ee6c8fffabbfa05c3d65100b03f548b7 sha256: 85d2382958c178491d3fe50d770a624621f5ab456beef7d31ac7521f780c9bc7
size: 650253 size: 640042
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/23.yaml url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/17.yaml
original: lts-20.23 original: lts-21.17

View file

@ -56,10 +56,10 @@ main = do
describe "hex strings" $ do describe "hex strings" $ do
prop "encoding and decoding are inverse" $ \x -> prop "encoding and decoding are inverse" $ \x ->
(decodeHexText . encodeHexText) x == x (decodeHexText . encodeHexText) x == x
describe "zToZGoTx" $ describe "Memo parsers" $
--prop "memo parsing" testMemoParser --prop "memo parsing" testMemoParser
do do
it "parse ZecWallet memo" $ do it "parse ZecWallet memo - Sapling" $ do
let m = let m =
runParser runParser
pZGoMemo pZGoMemo
@ -70,7 +70,7 @@ main = do
Right m' -> Right m' ->
m_session m' `shouldBe` m_session m' `shouldBe`
U.fromString "5d3d4494-51c0-432d-8495-050419957aea" U.fromString "5d3d4494-51c0-432d-8495-050419957aea"
it "parse YWallet memo" $ do it "parse YWallet memo - Sapling" $ do
let m = let m =
runParser runParser
pZGoMemo pZGoMemo
@ -81,90 +81,53 @@ main = do
Right m' -> Right m' ->
m_session m' `shouldBe` m_session m' `shouldBe`
U.fromString "ad8477d3-4fdd-4c97-90b2-76630b5f77e1" U.fromString "ad8477d3-4fdd-4c97-90b2-76630b5f77e1"
it "converts ZecWallet tx to ZGo tx" $ do it "parse Zingo memo - Sapling" $ do
let t = let m =
ZcashTx runParser
"someId" pZGoMemo
0.5 "Zingo memo"
50000000 "ZGO::5d3d4494-51c0-432d-8495-050419957aea\nReply to:\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e"
1602000 case m of
18732456 Left e -> putStrLn $ errorBundlePretty e
False Right m' ->
5 m_session m' `shouldBe`
"ZGO::5d3d4494-51c0-432d-8495-050419957aea\nReply-To:\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e" U.fromString "5d3d4494-51c0-432d-8495-050419957aea"
zToZGoTx t `shouldBe` it "parse ZecWallet memo - Orchard" $ do
ZGoTx let m =
Nothing runParser
"zs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e" pZGoMemo
"5d3d4494-51c0-432d-8495-050419957aea" "Zecwalllet memo"
5 "ZGO::5d3d4494-51c0-432d-8495-050419957aea\nReply-To:\nu17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x"
18732456 case m of
0.5 Left e -> putStrLn $ errorBundlePretty e
"someId" Right m' ->
"ZGO::5d3d4494-51c0-432d-8495-050419957aea\nReply-To:\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e" m_address m' `shouldBe`
it "converts YWallet tx to ZGo tx" $ do Just
let t = "u17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x"
ZcashTx it "parse YWallet memo - Orchard" $ do
"someId" let m =
0.5 runParser
50000000 pZGoMemo
1602000 "Ywallet memo"
18732456 "\128737MSG\nu17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x\n\nZGO::ad8477d3-4fdd-4c97-90b2-76630b5f77e1"
False case m of
5 Left e -> putStrLn $ errorBundlePretty e
"\128737MSG\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e\n\nZGO::ad8477d3-4fdd-4c97-90b2-76630b5f77e1" Right m' ->
zToZGoTx t `shouldBe` m_address m' `shouldBe`
ZGoTx Just
Nothing "u17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x"
"zs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e" it "parse Zingo memo - Orchard" $ do
"ad8477d3-4fdd-4c97-90b2-76630b5f77e1" let m =
5 runParser
18732456 pZGoMemo
0.5 "Zingo memo"
"someId" "ZGO::5d3d4494-51c0-432d-8495-050419957aea\nReply to:\nu17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x"
"\128737MSG\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e\n\nZGO::ad8477d3-4fdd-4c97-90b2-76630b5f77e1" case m of
it "converts ZecWallet payment tx to ZGo tx" $ do Left e -> putStrLn $ errorBundlePretty e
let t = Right m' ->
ZcashTx m_address m' `shouldBe`
"someId" Just
0.5 "u17n7hpwaujyq7ux8f9jpyymtnk5urw7pyrf60smp5mawy7jgz325hfvz3jn3zsfya8yxryf9q7ldk8nu8df0emra5wne28zq9d9nm2pu4x6qwjha565av9aze0xgujgslz74ufkj0c0cylqwjyrh9msjfh7jzal6d3qzrnhkkqy3pqm8j63y07jxj7txqeac982778rmt64f32aum94x"
50000000
1602000
18732456
False
5
"ZGOp::5d3d4494-51c0-432d-8495-050419957aea\nReply-To:\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e"
zToZGoTx t `shouldBe`
ZGoTx
Nothing
""
"5d3d4494-51c0-432d-8495-050419957aea"
5
18732456
0.5
"someId"
"ZGOp::5d3d4494-51c0-432d-8495-050419957aea\nReply-To:\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e"
it "converts YWallet payment tx to ZGo tx" $ do
let t =
ZcashTx
"someId"
0.5
50000000
1602000
18732456
False
5
"\128737MSG\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e\n\nZGOp::ad8477d3-4fdd-4c97-90b2-76630b5f77e1"
zToZGoTx t `shouldBe`
ZGoTx
Nothing
""
"ad8477d3-4fdd-4c97-90b2-76630b5f77e1"
5
18732456
0.5
"someId"
"\128737MSG\nzs1w6nkameazc5gujm69350syl5w8tgvyaphums3pw8eytzy5ym08x7dvskmykkatmwrucmgv3er8e\n\nZGOp::ad8477d3-4fdd-4c97-90b2-76630b5f77e1"
describe "PIN generator" $ do describe "PIN generator" $ do
it "should give a 7 digit" $ do it "should give a 7 digit" $ do
pin <- generatePin pin <- generatePin
@ -335,7 +298,7 @@ main = do
it "return owner by id" $ do it "return owner by id" $ do
req <- req <-
testGet testGet
"/api/ownerid" "/ownerid"
[ ("id", Just "627ad3492b05a76be3000001") [ ("id", Just "627ad3492b05a76be3000001")
, ("session", Just "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd") , ("session", Just "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd")
] ]
@ -362,6 +325,9 @@ main = do
"" ""
"" ""
"testToken4321" "testToken4321"
0
0
0
req <- req <-
testPostJson "/api/order" $ testPostJson "/api/order" $
A.object ["payload" A..= A.toJSON testOrder] A.object ["payload" A..= A.toJSON testOrder]
@ -389,6 +355,9 @@ main = do
"" ""
"" ""
"testToken4321" "testToken4321"
0
0
0
req <- req <-
testPostJson "/api/order" $ testPostJson "/api/order" $
A.object ["payload" A..= A.toJSON testOrder] A.object ["payload" A..= A.toJSON testOrder]
@ -416,6 +385,9 @@ main = do
"" ""
"" ""
"testToken4321" "testToken4321"
0
0
0
req <- req <-
testPostJson "/api/order" $ testPostJson "/api/order" $
A.object ["payload" A..= A.toJSON testOrder] A.object ["payload" A..= A.toJSON testOrder]
@ -728,6 +700,8 @@ main = do
"zxviews1qdjagrrpqqqqpq8es75mlu6rref0qyrstchf8dxzeygtsejwfqu8ckhwl2qj5m8am7lmupxk3vkvdjm8pawjpmesjfapvsqw96pa46c2z0kk7letrxf7mkltwz54fwpxc7kc79mm5kce3rwn5ssl009zwsra2spppwgrx25s9k5hq65f69l4jz2tjmqgy0pl49qmtaj3nudk6wglwe2hpa327hydlchtyq9av6wjd6hu68e04ahwk9a9n2kt0kj3nj99nue65awtu5cwwcpjs" "zxviews1qdjagrrpqqqqpq8es75mlu6rref0qyrstchf8dxzeygtsejwfqu8ckhwl2qj5m8am7lmupxk3vkvdjm8pawjpmesjfapvsqw96pa46c2z0kk7letrxf7mkltwz54fwpxc7kc79mm5kce3rwn5ssl009zwsra2spppwgrx25s9k5hq65f69l4jz2tjmqgy0pl49qmtaj3nudk6wglwe2hpa327hydlchtyq9av6wjd6hu68e04ahwk9a9n2kt0kj3nj99nue65awtu5cwwcpjs"
let vk2 = let vk2 =
"zxviews1qdjagrrpqqqqpq8es75mlufakef0qyrstchf8dxzeygtsejwfqu8ckhwl2qj5m8am7lmupxk3vkvdjm8pawjpmesjfapvsqw96pa46c2z0kk7letrxf7mkltwz54fwpxc7kc79mm5kce3rwn5ssl009zwsra2spppwgrx25s9k5hq65f69l4jz2tjmqgy0pl49qmtaj3nudk6wglwe2hpa327hydlchtyq9av6wjd6hu68e04ahwk9a9n2kt0kj3nj99nue65awtu5cwwcpjs" "zxviews1qdjagrrpqqqqpq8es75mlufakef0qyrstchf8dxzeygtsejwfqu8ckhwl2qj5m8am7lmupxk3vkvdjm8pawjpmesjfapvsqw96pa46c2z0kk7letrxf7mkltwz54fwpxc7kc79mm5kce3rwn5ssl009zwsra2spppwgrx25s9k5hq65f69l4jz2tjmqgy0pl49qmtaj3nudk6wglwe2hpa327hydlchtyq9av6wjd6hu68e04ahwk9a9n2kt0kj3nj99nue65awtu5cwwcpjs"
let vk3 =
"uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm"
it "returns 401 with bad session" $ do it "returns 401 with bad session" $ do
req <- req <-
testPostJson "/api/ownervk" $ testPostJson "/api/ownervk" $
@ -768,7 +742,7 @@ main = do
[("session", Just "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd")] [("session", Just "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd")]
req req
getResponseStatus res `shouldBe` badRequest400 getResponseStatus res `shouldBe` badRequest400
it "succeeds with correct key" $ do it "succeeds with correct Sapling key" $ do
req <- req <-
testPostJson "/api/ownervk" $ testPostJson "/api/ownervk" $
A.object ["payload" A..= (vk1 :: String)] A.object ["payload" A..= (vk1 :: String)]
@ -778,6 +752,26 @@ main = do
[("session", Just "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd")] [("session", Just "35bfb9c2-9ad2-4fe5-adda-99d63b8dcdcd")]
req req
getResponseStatus res `shouldBe` created201 getResponseStatus res `shouldBe` created201
it "succeeds with correct Unified key and UA" $ do
req <-
testPostJson "/api/ownervk" $
A.object ["payload" A..= (vk3 :: String)]
res <-
httpLBS $
setRequestQueryString
[("session", Just "35bfb9c2-9ad2-4fe5-daad-99d63b8dcdaa")]
req
getResponseStatus res `shouldBe` created201
xit "succeeds with correct Unified key and Sapling address" $ do
req <-
testPostJson "/api/ownervk" $
A.object ["payload" A..= (vk3 :: String)]
res <-
httpLBS $
setRequestQueryString
[("session", Just "35bfb9c2-a92d-4fe5-daad-99d63b8dcdaa")]
req
getResponseStatus res `shouldBe` created201
around handleDb $ around handleDb $
describe "Database actions" $ do describe "Database actions" $ do
describe "authentication" $ do describe "authentication" $ do
@ -866,6 +860,9 @@ main = do
"" ""
"" ""
"testToken1234" "testToken1234"
0
0
0
let ordTest = val myOrder let ordTest = val myOrder
case ordTest of case ordTest of
Doc oT -> access p master "test" (insert_ "orders" oT) Doc oT -> access p master "test" (insert_ "orders" oT)
@ -909,7 +906,7 @@ main = do
xit "logins are added to db" $ \p -> do xit "logins are added to db" $ \p -> do
_ <- _ <-
access p master "test" (Database.MongoDB.delete (select [] "txs")) access p master "test" (Database.MongoDB.delete (select [] "txs"))
_ <- scanZcash loadedConfig p _ <- scanZcash' loadedConfig p
threadDelay 1000000 threadDelay 1000000
t <- access p master "test" $ findOne (select [] "txs") t <- access p master "test" $ findOne (select [] "txs")
let s = parseZGoTxBson =<< t let s = parseZGoTxBson =<< t
@ -922,7 +919,7 @@ main = do
master master
"test" "test"
(Database.MongoDB.delete (select [] "payments")) (Database.MongoDB.delete (select [] "payments"))
_ <- scanZcash loadedConfig p _ <- scanZcash' loadedConfig p
threadDelay 1000000 threadDelay 1000000
t <- access p master "test" $ findOne (select [] "payments") t <- access p master "test" $ findOne (select [] "payments")
let s = (cast' . Doc) =<< t let s = (cast' . Doc) =<< t
@ -1158,17 +1155,40 @@ unwrapDoc _ = []
startAPI :: Config -> IO () startAPI :: Config -> IO ()
startAPI config = do startAPI config = do
putStrLn "Starting test server ..." putStrLn "Starting test server ..."
pipe <- connect $ host "127.0.0.1" pipe <- connect $ host $ c_dbHost config
c <- access pipe master "zgo" (auth "zgo" "zcashrules") c <- access pipe master "zgo" (auth (c_dbUser config) (c_dbPassword config))
let appRoutes = routes pipe config let appRoutes = routes pipe config
_ <- forkIO (scotty 3000 appRoutes) _ <- forkIO (scotty 3000 appRoutes)
_ <- _ <-
access pipe master "test" (Database.MongoDB.delete (select [] "wootokens")) access
_ <- access pipe master "test" (Database.MongoDB.delete (select [] "users")) pipe
_ <- access pipe master "test" (Database.MongoDB.delete (select [] "items")) master
_ <- access pipe master "test" (Database.MongoDB.delete (select [] "orders")) (c_dbName config)
(Database.MongoDB.delete (select [] "wootokens"))
_ <- _ <-
access pipe master "test" (Database.MongoDB.delete (select [] "xerotokens")) access
pipe
master
(c_dbName config)
(Database.MongoDB.delete (select [] "users"))
_ <-
access
pipe
master
(c_dbName config)
(Database.MongoDB.delete (select [] "items"))
_ <-
access
pipe
master
(c_dbName config)
(Database.MongoDB.delete (select [] "orders"))
_ <-
access
pipe
master
(c_dbName config)
(Database.MongoDB.delete (select [] "xerotokens"))
let myUser = let myUser =
User User
(Just (read "6272a90f2b05a74cf1000001" :: ObjectId)) (Just (read "6272a90f2b05a74cf1000001" :: ObjectId))
@ -1193,8 +1213,25 @@ startAPI config = do
1613487 1613487
"8ea140fbb30615d6cae383c4f62f3ad9afb10b804f63138d5b53990bd56e0162" "8ea140fbb30615d6cae383c4f62f3ad9afb10b804f63138d5b53990bd56e0162"
True True
let myUser3 =
User
(Just (read "6272a90f2b05a74cf1500003" :: ObjectId))
"u15hjz9v46azzmdept050heh8795qxzwy2pykg097lg69jpk4qzah90cj2q4amq0c07gta60x8qgw00qewcy3hg9kv9h6zjkh3jc66vr40u6uu2dxmqkqhypud95vm0gq7y5ga7c8psdqgthsrwvgd676a2pavpcd4euwwapgackxa3qhvga0wnl0k6vncskxlq94vqwjd7zepy3qd5jh"
"35bfb9c2-9ad2-4fe5-daad-99d63b8dcdaa"
1613487
"8ea140fbb30615d6cae383c4f62f3ad9afb10b804f63138d5b53990bd56e0162"
True
let myUser4 =
User
(Just (read "6272a90f2b05a74cf7500003" :: ObjectId))
"zs1fau9x305eztcdm5f08q9uc4hmvvjpjrgjcwcj0mjwhd83pdj0j92rxwqp6zkjmz3e49ej4xrcc8"
"35bfb9c2-a92d-4fe5-daad-99d63b8dcdaa"
1613487
"8ea140fbb30615d6cae383c4f62f3ad9afb10b804f63138d5b53990bd56e0162"
True
let userList = let userList =
map unwrapDoc $ filter filterDocs $ val <$> [myUser, myUser1, myUser2] map unwrapDoc $
filter filterDocs $ val <$> [myUser, myUser1, myUser2, myUser3, myUser4]
_ <- access pipe master "test" (insertAll_ "users" userList) _ <- access pipe master "test" (insertAll_ "users" userList)
let myOwner = let myOwner =
Owner Owner
@ -1223,6 +1260,7 @@ startAPI config = do
False False
"" ""
"" ""
False
let myOwner1 = let myOwner1 =
Owner Owner
(Just (read "627ad3492b05a76be3000008")) (Just (read "627ad3492b05a76be3000008"))
@ -1250,6 +1288,63 @@ startAPI config = do
False False
"" ""
"" ""
False
let myOwner2 =
Owner
(Just (read "627ad3492b05a76be3700008"))
"u15hjz9v46azzmdept050heh8795qxzwy2pykg097lg69jpk4qzah90cj2q4amq0c07gta60x8qgw00qewcy3hg9kv9h6zjkh3jc66vr40u6uu2dxmqkqhypud95vm0gq7y5ga7c8psdqgthsrwvgd676a2pavpcd4euwwapgackxa3qhvga0wnl0k6vncskxlq94vqwjd7zepy3qd5jh"
"Test shop 3"
"usd"
False
0
False
0
"Roxy"
"Foo"
"roxy@zgo.cash"
"1 Main St"
"Mpls"
"Minnesota"
"55401"
""
"missyfoo.io"
"United States"
True
False
False
(UTCTime (fromGregorian 2024 8 6) (secondsToDiffTime 0))
False
""
""
False
let myOwner3 =
Owner
(Just (read "627ad3492b05a76be3750008"))
"zs1fau9x305eztcdm5f08q9uc4hmvvjpjrgjcwcj0mjwhd83pdj0j92rxwqp6zkjmz3e49ej4xrcc8"
"Test shop 4"
"usd"
False
0
False
0
"Roxy"
"Foo"
"roxy@zgo.cash"
"1 Main St"
"Mpls"
"Minnesota"
"55401"
""
"missyfoo.io"
"United States"
True
False
False
(UTCTime (fromGregorian 2024 8 6) (secondsToDiffTime 0))
False
""
""
False
_ <- access pipe master "test" (Database.MongoDB.delete (select [] "owners")) _ <- access pipe master "test" (Database.MongoDB.delete (select [] "owners"))
let o = val myOwner let o = val myOwner
case o of case o of
@ -1259,6 +1354,14 @@ startAPI config = do
case o1 of case o1 of
Doc d1 -> access pipe master "test" (insert_ "owners" d1) Doc d1 -> access pipe master "test" (insert_ "owners" d1)
_ -> fail "Couldn't save Owner1 in DB" _ -> fail "Couldn't save Owner1 in DB"
let o2 = val myOwner2
case o2 of
Doc d2 -> access pipe master "test" (insert_ "owners" d2)
_ -> fail "Couldn't save Owner2 in DB"
let o3 = val myOwner3
case o3 of
Doc d3 -> access pipe master "test" (insert_ "owners" d3)
_ -> fail "Couldn't save Owner2 in DB"
_ <- access pipe master "test" (Database.MongoDB.delete (select [] "orders")) _ <- access pipe master "test" (Database.MongoDB.delete (select [] "orders"))
myTs <- liftIO getCurrentTime myTs <- liftIO getCurrentTime
let myOrder = let myOrder =
@ -1277,6 +1380,9 @@ startAPI config = do
"" ""
"" ""
"testToken1234" "testToken1234"
0
0
0
let ordTest = val myOrder let ordTest = val myOrder
case ordTest of case ordTest of
Doc oT -> access pipe master "test" (insert_ "orders" oT) Doc oT -> access pipe master "test" (insert_ "orders" oT)
@ -1342,7 +1448,10 @@ instance Arbitrary ZGoOrder where
pd <- arbitrary pd <- arbitrary
eI <- arbitrary eI <- arbitrary
sc <- arbitrary sc <- arbitrary
ZGoOrder i a s ts c cur p t tZ l pd eI sc <$> arbitrary tk <- arbitrary
qT <- arbitrary
qV <- arbitrary
ZGoOrder i a s ts c cur p t tZ l pd eI sc tk qT qV <$> arbitrary
instance Arbitrary LineItem where instance Arbitrary LineItem where
arbitrary = do arbitrary = do
@ -1381,7 +1490,33 @@ instance Arbitrary Owner where
exp <- arbitrary exp <- arbitrary
payconf <- arbitrary payconf <- arbitrary
vk <- arbitrary vk <- arbitrary
Owner i a n c t tV v vV f l e s ct st p ph w co paid zats inv exp payconf vk <$> cT <- arbitrary
Owner
i
a
n
c
t
tV
v
vV
f
l
e
s
ct
st
p
ph
w
co
paid
zats
inv
exp
payconf
vk
cT <$>
arbitrary arbitrary
instance Arbitrary Item where instance Arbitrary Item where

View file

@ -1,11 +1,11 @@
cabal-version: 1.12 cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.35.1. -- This file has been generated from package.yaml by hpack version 0.35.2.
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: zgo-backend name: zgo-backend
version: 1.6.0 version: 1.8.0
synopsis: Haskell Back-end for the ZGo point-of-sale application synopsis: Haskell Back-end for the ZGo point-of-sale application
description: Please see the README at <https://git.vergara.tech/Vergara_Tech//zgo-backend#readme> description: Please see the README at <https://git.vergara.tech/Vergara_Tech//zgo-backend#readme>
category: Web category: Web