From b35b89fbb4a95036e58310ed63c794f043241347 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 4 May 2023 15:26:49 -0500 Subject: [PATCH] Implement Orchard Tx decoding and tests --- Makefile | 4 +- librustzcash-wrapper/src/lib.rs | 2 +- src/C/Zcash.chs | 28 +++++-------- src/HaskellZcash/Orchard.hs | 34 ++++++++++++++++ src/HaskellZcash/Sapling.hs | 8 ++++ src/HaskellZcash/Types.hs | 63 ++++++++++++++++++++++++++++++ src/HaskellZcash/Utils.hs | 34 ++++++++++++++++ src/Zcash.hs | 69 --------------------------------- test/Spec.hs | 63 +++++++++++++++++++++++++++++- zcash-haskell.cabal | 5 ++- 10 files changed, 217 insertions(+), 93 deletions(-) create mode 100644 src/HaskellZcash/Orchard.hs create mode 100644 src/HaskellZcash/Sapling.hs create mode 100644 src/HaskellZcash/Types.hs create mode 100644 src/HaskellZcash/Utils.hs delete mode 100644 src/Zcash.hs diff --git a/Makefile b/Makefile index 3189202..2c73f57 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,10 @@ rustlib := librustzcash-wrapper/target/x86_64-unknown-linux-gnu/debug .PHONY: all -all: rust-wrapper haskell +all: haskell haskell: src/Zcash.hs src/C/Zcash.chs package.yaml stack.yaml $(rustlib)/rustzcash_wrapper.h $(rustlib)/librustzcash_wrapper.a $(rustlib)/librustzcash_wrapper.so $(rustlib)/rustzcash_wrapper-uninstalled.pc stack build -rust-wrapper: librustzcash-wrapper/src/lib.rs librustzcash-wrapper/Cargo.toml +$(rustlib)/rustzcash_wrapper.h: librustzcash-wrapper/src/lib.rs librustzcash-wrapper/Cargo.toml cd librustzcash-wrapper && cargo +nightly cbuild diff --git a/librustzcash-wrapper/src/lib.rs b/librustzcash-wrapper/src/lib.rs index 3662807..0aaf6ee 100644 --- a/librustzcash-wrapper/src/lib.rs +++ b/librustzcash-wrapper/src/lib.rs @@ -209,7 +209,7 @@ pub extern "C" fn rust_wrapper_ufvk_decode( } #[no_mangle] -pub extern "C" fn rust_wrapper_ua_note_decrypt( +pub extern "C" fn rust_wrapper_orchard_note_decrypt( key: *const u8, key_len: usize, note: *const u8, diff --git a/src/C/Zcash.chs b/src/C/Zcash.chs index e7eed78..5c1b875 100644 --- a/src/C/Zcash.chs +++ b/src/C/Zcash.chs @@ -13,6 +13,7 @@ import qualified Data.ByteString as BS import Codec.Borsh import Data.Text (Text) import Data.Word +import Data.Int import Data.Structured import Foreign.C.Types import Foreign.Rust.Marshall.External @@ -22,25 +23,8 @@ import Foreign.Rust.Serialisation.Raw import Foreign.Rust.Serialisation.Raw.Base16 import qualified Generics.SOP as SOP import qualified GHC.Generics as GHC +import HaskellZcash.Types -data RawData = RawData { hrp :: BS.ByteString, bytes :: BS.ByteString} - deriving stock (Prelude.Show, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving anyclass (Data.Structured.Show) - deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct RawData - -data UnifiedFullViewingKey = - UnifiedFullViewingKey - { net :: Word8 - , o_key :: BS.ByteString - , s_key :: BS.ByteString - , t_key :: BS.ByteString - } - deriving stock (Eq, Prelude.Show, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving anyclass (Data.Structured.Show) - deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct UnifiedFullViewingKey - {# fun unsafe rust_wrapper_f4jumble as rustWrapperF4Jumble { toBorshVar* `BS.ByteString'& @@ -81,3 +65,11 @@ data UnifiedFullViewingKey = } -> `()' #} + +{# fun unsafe rust_wrapper_orchard_note_decrypt as rustWrapperOrchardNoteDecode + { toBorshVar* `BS.ByteString'& + , toBorshVar* `OrchardAction'& + , getVarBuffer `Buffer OrchardDecodedAction'& + } + -> `()' + #} diff --git a/src/HaskellZcash/Orchard.hs b/src/HaskellZcash/Orchard.hs new file mode 100644 index 0000000..46032b7 --- /dev/null +++ b/src/HaskellZcash/Orchard.hs @@ -0,0 +1,34 @@ +module HaskellZcash.Orchard where + +import C.Zcash + ( rustWrapperIsUA + , rustWrapperOrchardNoteDecode + , rustWrapperUfvkDecode + ) +import qualified Data.ByteString as BS +import Foreign.Rust.Marshall.Variable +import HaskellZcash.Types + +-- | Check if given bytestring is a valid encoded unified address +isValidUnifiedAddress :: BS.ByteString -> Bool +isValidUnifiedAddress = rustWrapperIsUA + +-- | Attempt to decode the given bytestring into a Unified Full Viewing Key +decodeUfvk :: BS.ByteString -> Maybe UnifiedFullViewingKey +decodeUfvk str = + case net decodedKey of + 0 -> Nothing + _ -> Just decodedKey + where + decodedKey = (withPureBorshVarBuffer . rustWrapperUfvkDecode) str + +decryptOrchardAction :: + OrchardAction -> UnifiedFullViewingKey -> Maybe OrchardDecodedAction +decryptOrchardAction encAction key = + case a_value decodedAction of + 0 -> Nothing + _ -> Just decodedAction + where + decodedAction = + withPureBorshVarBuffer $ + rustWrapperOrchardNoteDecode (o_key key) encAction diff --git a/src/HaskellZcash/Sapling.hs b/src/HaskellZcash/Sapling.hs new file mode 100644 index 0000000..e77285c --- /dev/null +++ b/src/HaskellZcash/Sapling.hs @@ -0,0 +1,8 @@ +module HaskellZcash.Sapling where + +import C.Zcash (rustWrapperIsShielded) +import qualified Data.ByteString as BS + +-- | Check if given bytesting is a valid encoded shielded address +isValidShieldedAddress :: BS.ByteString -> Bool +isValidShieldedAddress = rustWrapperIsShielded diff --git a/src/HaskellZcash/Types.hs b/src/HaskellZcash/Types.hs new file mode 100644 index 0000000..e3966f6 --- /dev/null +++ b/src/HaskellZcash/Types.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE UndecidableInstances #-} + +module HaskellZcash.Types where + +import qualified Data.ByteString as BS +import Codec.Borsh +import Data.Word +import Data.Int +import Data.Structured +import qualified Generics.SOP as SOP +import qualified GHC.Generics as GHC + +data RawData = RawData { hrp :: BS.ByteString, bytes :: BS.ByteString} + deriving stock (Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct RawData + +data UnifiedFullViewingKey = + UnifiedFullViewingKey + { net :: Word8 + , o_key :: BS.ByteString + , s_key :: BS.ByteString + , t_key :: BS.ByteString + } + deriving stock (Eq, Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct UnifiedFullViewingKey + +data OrchardAction = + OrchardAction + { nf :: BS.ByteString + , rk :: BS.ByteString + , cmx :: BS.ByteString + , eph_key :: BS.ByteString + , enc_ciphertext :: BS.ByteString + , out_ciphertext :: BS.ByteString + , cv :: BS.ByteString + , auth :: BS.ByteString + } + deriving stock (Eq, Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct OrchardAction + +data OrchardDecodedAction = + OrchardDecodedAction + { a_value :: Int64 + , a_recipient :: BS.ByteString + , a_memo :: BS.ByteString + } + deriving stock (Eq, Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct OrchardDecodedAction + + diff --git a/src/HaskellZcash/Utils.hs b/src/HaskellZcash/Utils.hs new file mode 100644 index 0000000..0bc6766 --- /dev/null +++ b/src/HaskellZcash/Utils.hs @@ -0,0 +1,34 @@ +module HaskellZcash.Utils where + +import C.Zcash + ( rustWrapperBech32Decode + , rustWrapperF4Jumble + , rustWrapperF4UnJumble + ) + +import qualified Data.ByteString as BS +import Foreign.Rust.Marshall.Variable +import HaskellZcash.Types + +-- | Helper function to turn a hex-encoded strings to bytestring +decodeHexText :: String -> BS.ByteString +decodeHexText h = BS.pack $ hexRead h + where + hexRead hexText + | null chunk = [] + | otherwise = + fromIntegral (read ("0x" <> chunk)) : hexRead (drop 2 hexText) + where + chunk = take 2 hexText + +-- | Decode the given bytestring using Bech32 +decodeBech32 :: BS.ByteString -> RawData +decodeBech32 = withPureBorshVarBuffer . rustWrapperBech32Decode + +-- | Apply the F4Jumble transformation to the given bytestring +f4Jumble :: BS.ByteString -> BS.ByteString +f4Jumble = withPureBorshVarBuffer . rustWrapperF4Jumble + +-- | Apply the inverse F4Jumble transformation to the given bytestring +f4UnJumble :: BS.ByteString -> BS.ByteString +f4UnJumble = withPureBorshVarBuffer . rustWrapperF4UnJumble diff --git a/src/Zcash.hs b/src/Zcash.hs deleted file mode 100644 index 66c7d7f..0000000 --- a/src/Zcash.hs +++ /dev/null @@ -1,69 +0,0 @@ -module Zcash - ( f4Jumble - , f4UnJumble - , isValidUnifiedAddress - , isValidShieldedAddress - , decodeBech32 - , decodeUfvk - ) where - -import C.Zcash - ( RawData - , UnifiedFullViewingKey(..) - , rustWrapperBech32Decode - , rustWrapperF4Jumble - , rustWrapperF4UnJumble - , rustWrapperIsShielded - , rustWrapperIsUA - , rustWrapperUfvkDecode - ) -import qualified Data.ByteString as BS -import Foreign.Rust.Marshall.Fixed -import Foreign.Rust.Marshall.Variable - --- | Apply the F4Jumble transformation to the given bytestring -f4Jumble :: BS.ByteString -> BS.ByteString -f4Jumble = withPureBorshVarBuffer . rustWrapperF4Jumble - --- | Apply the inverse F4Jumble transformation to the given bytestring -f4UnJumble :: BS.ByteString -> BS.ByteString -f4UnJumble = withPureBorshVarBuffer . rustWrapperF4UnJumble - --- | Check if given bytestring is a valid encoded unified address -isValidUnifiedAddress :: BS.ByteString -> Bool -isValidUnifiedAddress = rustWrapperIsUA - --- | Check if given bytesting is a valid encoded shielded address -isValidShieldedAddress :: BS.ByteString -> Bool -isValidShieldedAddress = rustWrapperIsShielded - --- | Decode the given bytestring using Bech32 -decodeBech32 :: BS.ByteString -> RawData -decodeBech32 = withPureBorshVarBuffer . rustWrapperBech32Decode - --- | Attempt to decode the given bytestring into a Unified Full Viewing Key -decodeUfvk :: BS.ByteString -> Maybe UnifiedFullViewingKey -decodeUfvk str = - case net decodedKey of - 0 -> Nothing - _ -> Just decodedKey - where - decodedKey = (withPureBorshVarBuffer . rustWrapperUfvkDecode) str - -deriveOvk :: BS.ByteString -> BS.ByteString -deriveOvk fvk = undefined -- BS.takeEnd (lovk / 8) $ r k (t ak nk) - --where - --lovk :: Int - --lovk = 256 - --ak :: BS.ByteString - --ak = BS.take 32 fvk - --nk :: BS.ByteString - --nk = BS.take 32 $ BS.takeEnd 64 fvk - --rivk :: BS.ByteString - --rivk = BS.takeEnd 32 fvk - --k :: BS.ByteString - --k = i2lebsp rivk - --r :: BS.ByteString -> BS.ByteString -> BS.ByteString - --r ki ti = blake2b512 "Zcash_ExpandSeed" (BS.append (lebs2osp ki) ti) - --t :: BS.ByteString -> BS.ByteString -> BS.ByteString - --t a n = BS.append (BS.cons 0x82 (i2leosp a)) (i2leosp n) diff --git a/test/Spec.hs b/test/Spec.hs index 835d820..4a01f68 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,11 +1,18 @@ {-# LANGUAGE OverloadedStrings #-} -import C.Zcash (RawData(..), UnifiedFullViewingKey(..), rustWrapperIsUA) +import C.Zcash (rustWrapperIsUA) import qualified Data.ByteString as BS import qualified Data.Text.Encoding as E import Data.Word +import HaskellZcash.Orchard +import HaskellZcash.Types + ( OrchardAction(..) + , OrchardDecodedAction(..) + , RawData(..) + , UnifiedFullViewingKey(..) + ) +import HaskellZcash.Utils import Test.Hspec -import Zcash main :: IO () main = do @@ -238,3 +245,55 @@ main = do let fakeUvk = "uview1u83changinga987bundchofch4ract3r5x8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" decodeUfvk fakeUvk `shouldBe` Nothing + describe "Decode Orchard tx" $ do + let uvk = + "uview1u833rp8yykd7h4druwht6xp6k8krle45fx8hqsw6vzw63n24atxpcatws82z092kryazuu6d7rayyut8m36wm4wpjy2z8r9hj48fx5pf49gw4sjrq8503qpz3vqj5hg0vg9vsqeasg5qjuyh94uyfm7v76udqcm2m0wfc25hcyqswcn56xxduq3xkgxkr0l73cjy88fdvf90eq5fda9g6x7yv7d0uckpevxg6540wc76xrc4axxvlt03ptaa2a0rektglmdy68656f3uzcdgqqyu0t7wk5cvwghyyvgqc0rp3vgu5ye4nd236ml57rjh083a2755qemf6dk6pw0qrnfm7246s8eg2hhzkzpf9h73chhng7xhmyem2sjh8rs2m9nhfcslsgenm" + let res = decodeUfvk uvk + let a = + OrchardAction + (decodeHexText + "248b16d98dfa33f7ba69a0610a63b606699da76c288840b81d7691ee42764416") + (decodeHexText + "17fcc27cce560733edaf91439a8020c4a029a4e7d5893ce024d5ff4b40bbd0a9") + (decodeHexText + "34796d541864832acca43f083892e98a46c912802d5643672d3f25bea177c61c") + (decodeHexText + "a6d2ca10e3fc7446e372266ef45ee3dc0ba373bd378e6bf3092519a7f272bd8c") + (decodeHexText + "08beafdf59110b5d045e4acc13731ef1a27bfa3a9cabe1d575640c18f18ee6697fbb132d36e982ae3eadf5f37fd35f42c2bb07def14759deab1fbe2f98dc1d5913e4a6ef388b714e2cfd6d89ba2302800e02ab5f45e0e02e3895448518cd8afd2c37bb48a66d8b988a37de9d0838d92876894a311bb9f314ba842e5c18ff7a3d8c7f0ff1a7209e2d661595db8f4a4aa267b9593258914bf63c09286eeda7c9b27ddbb4646208c0d03a8fbdc5d96633335a5a65316f5b25189bdce735bdea7e900de56d3b475ae51b7c35eb7ae79ba104baeb0a5a09d1cd8bb347ab34fb26d62ddbf024f5394710626ec0a665b9c917e65b00256db635145164a0329db7bc5358f435d573b2662adf8a6128801825ec8fb7d8aeef567d35c875ddd784fceb7620355e3f056a648b39b4b2d29a1f5e7b7c4ec5fd2b1874ff1e832b308f8644e83878d90582b9a6fd6c293e19dd3e24dbe1b4c15c96608169843d46551900a8cb787b15f0f1696b736dd4c8ebacf1e3288b14e469bdc004fa8557d6b1395700eaba59334906bb012f876e4cd7acd2157719ebd2e28bd0cd4ab4ac458f8848e1c30e729803dd47102200fe703932a15c3618862ec83b40d3aa0ec2343641bcb9afbf931ab21aa4afdbe7e51deca24283c2ccab0eef6e07aac5a4bf3a775bf7d2ddfc8d8766c3bf8e35df1435cf515d93c3b9549477bd9f53d133f05dd256fbcc0b13a63e3e7f8cce6301ab4f19c114f5af079f8c581537458e861b553218a890ea3e77fb99781c7088cd43c67c155ec611c1148721cab5fd0168e4a5ec390b506ec44145474c") + (decodeHexText + "1e40d33446d9f0f0fad40f8829c1ffe860c11c3439e2c15d37c6c40282f9e933dc01798c800e6c92edb4d20478b92559510eda67f3855f68f5ab22ca31e1885c7fa9d4c9ebfb62ceb5e73267bcad0ba7") + (decodeHexText + "63d0d6e8e92691f700bf8af246dcd4ae1041b13e3969f7a9d819a06e0f9429bc") + (decodeHexText + "fe362be160accf2794841c244e8d80bbeb80b9bc95bb653d297a98d32bddf5a05dd5f874891d55924a83f722f75f576f63796770c31074067694cffb2cce7a2a") + let b = + OrchardAction + (decodeHexText + "8921446787f1bd28fa0e4cc5c945ea7fc71165a25f40cd2a325dae0c4467d12c") + (decodeHexText + "240b08b7861aa78989c68cbedd0038af9b3e3456bdc7ff582d597df571d54da2") + (decodeHexText + "e1bc8ccba69ab9f429bf735417aa005cf439d27500b0d3086dbf1be764b42a36") + (decodeHexText + "c89c58ef8553e7d09ba4090654edd1a8c98763c44d3dfb9dad18286c7ef363ae") + (decodeHexText + "0eee1ca5a3a4959cd4b8bc277e6e633f950680c4acb978c14ad8d944a784f46771c9d666a203ca3ac693943d79dd23f8b76a734a62e81932cbe98e8c851f47a11aaef50249e53151f38f88262a4bae8cf26f5f8b2db1d165aff9b57b64713a677c167608585c038e34ca7bbe468e5f86475ccec0a4a8b9a43b56e342e77a6bd09415787c9f4a1c6f20599f57545f1ac32c3a338d7a5bb2d35456adb880cb65c1455969e10df5d94b8c74b244e7093b1a88cc10697a7c2f4d34b6eae3296e64b820573b4d52e06b4427af5b8f5d6722d3a93fd85da615fceac732976ad2c1be4150b4821c149521f5419ea0746fb132d47f593cfc8a3aab6b2b4480c12fadf21280ccd3142e7188d9e5aef3fcd8c5dc0c066dc975bead023ef7f89a486b615b146110ae68b703a8349a5fc225b26a08b2adaf36fb44c9ad1be59d7ced134eb84e3f0b4aec19b71b2d26e910628a11446b97c5e6bbf97e1befa4e04b5947f83c65161b92f58088d28e57adc2a2873e27008e29772c5803502842045cb355d1ea5a9d27c2683dcb38cb49d26af39625ba99b1342f700387b939e7ff6c129417ca8836fe1e96331e35c8bc0763879e8c17cd4535fbcb27a2785c0a47294e07cb54837bb997df34882ce0bececc6adca365c76fc7533cf0503458937dcfb6058b016dbbd399b9f0cca44cbc881016f4957b5e10daada3393d5b2a4cb15ed983506d4d264f9855ce2ef87a7d4a1fc03293a22c28a53c4455447d546813fa33008e5d2d81848825fae2f437ab9575ba99c230e78f4b23e575e7647beff0e4c4e2b0a1f7320e9460") + (decodeHexText + "d727aeec27bb0f7463c6ed4f5b3f4085cfd3e7218478db0dcebfca875e025320fb64bc4062251823859e963446cadd9924c559e5f981480df5a4f036daf5a8033d4c8241e128902aa1aeaf6adc149730") + (decodeHexText + "98e72813aeb6ea05347798e35379bc881d9cf2b37d38850496ee956fbecd8eab") + (decodeHexText + "cb9926f519041343c957a74f2f67900ed3d250c4dbcd26b9e2addd5247b841a9fde2219d2ef8c9ae8145fecc7792ca6770830c58c95648087f3c8a0a69369402") + let decryptedNote = decryptOrchardAction a =<< res + let decryptedNote2 = decryptOrchardAction b =<< res + describe "First action (sender)" $ do + it "Decryption fails " $ do decryptedNote `shouldBe` Nothing + describe "Second action (recipient)" $ do + it "Decryption succeeds" $ do decryptedNote2 `shouldNotBe` Nothing + it "Tx amount is validated" $ do + (a_value <$> decryptedNote2) `shouldBe` Just 3000 + it "Memo is validated" $ do + let msg = maybe "" a_memo decryptedNote2 + msg `shouldBe` + "Hello World!\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL\NUL" diff --git a/zcash-haskell.cabal b/zcash-haskell.cabal index 8815cba..a5ae1e3 100644 --- a/zcash-haskell.cabal +++ b/zcash-haskell.cabal @@ -27,7 +27,10 @@ source-repository head library exposed-modules: C.Zcash - Zcash + HaskellZcash.Orchard + HaskellZcash.Sapling + HaskellZcash.Types + HaskellZcash.Utils other-modules: Paths_zcash_haskell hs-source-dirs: