Implement Frontier for Sapling #97
6 changed files with 156 additions and 73 deletions
|
@ -5,6 +5,12 @@ 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).
|
||||||
|
|
||||||
|
## [0.7.2.0]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Modified Sapling commitment trees to use Frontier
|
||||||
|
|
||||||
## [0.7.1.1]
|
## [0.7.1.1]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1369,6 +1369,31 @@ pub extern "C" fn rust_wrapper_bech32_encode(
|
||||||
marshall_to_haskell_var(&string, out, out_len, RW);
|
marshall_to_haskell_var(&string, out, out_len, RW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn rust_wrapper_read_sapling_frontier(
|
||||||
|
tree: *const u8,
|
||||||
|
tree_len: usize,
|
||||||
|
out: *mut u8,
|
||||||
|
out_len: &mut usize
|
||||||
|
){
|
||||||
|
let tree_in: Vec<u8> = marshall_from_haskell_var(tree, tree_len, RW);
|
||||||
|
let tree_reader = Cursor::new(tree_in);
|
||||||
|
let comm_tree: CommitmentTree<Node, SAPLING_DEPTH> = read_commitment_tree(tree_reader).unwrap();
|
||||||
|
//let comm_tree: Frontier<MerkleHashOrchard, 32> = read_frontier_v1(tree_reader).unwrap();
|
||||||
|
let frontier: Frontier<Node, SAPLING_DEPTH> = comm_tree.to_frontier();
|
||||||
|
match frontier.value() {
|
||||||
|
Some(f1) => {
|
||||||
|
let (pos, leaf, omm) = f1.clone().into_parts();
|
||||||
|
let f = Hfrontier { position: <u64>::from(pos), leaf: Hhex { bytes: leaf.to_bytes().to_vec()}, ommers: omm.iter().map(|&x| x.to_bytes().to_vec()).collect()};
|
||||||
|
marshall_to_haskell_var(&f, out, out_len, RW);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let f0 = Hfrontier { position: 0, leaf: Hhex { bytes: vec![0]}, ommers: vec![vec![0]]};
|
||||||
|
marshall_to_haskell_var(&f0, out, out_len, RW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn rust_wrapper_read_sapling_commitment_tree(
|
pub extern "C" fn rust_wrapper_read_sapling_commitment_tree(
|
||||||
tree: *const u8,
|
tree: *const u8,
|
||||||
|
@ -1378,39 +1403,21 @@ pub extern "C" fn rust_wrapper_read_sapling_commitment_tree(
|
||||||
out: *mut u8,
|
out: *mut u8,
|
||||||
out_len: &mut usize
|
out_len: &mut usize
|
||||||
){
|
){
|
||||||
let tree_in: Vec<u8> = marshall_from_haskell_var(tree, tree_len, RW);
|
let tree_in: Hfrontier = marshall_from_haskell_var(tree, tree_len, RW);
|
||||||
let tree_reader = Cursor::new(tree_in);
|
let leaf = Node::from_bytes(to_array(tree_in.leaf.bytes)).unwrap();
|
||||||
let mut ct = read_commitment_tree::<Node, Cursor<Vec<u8>>, SAPLING_DEPTH>(tree_reader);
|
let mut comm_tree = NonEmptyFrontier::from_parts(Position::from(tree_in.position), leaf, tree_in.ommers.iter().map(|x| Node::from_bytes(to_array(x.clone())).unwrap() ).collect()).unwrap();
|
||||||
match ct {
|
let node_in: Vec<u8> = marshall_from_haskell_var(node, node_len, RW);
|
||||||
Ok(mut comm_tree) => {
|
let sap_note_comm = SaplingNoteCommitment::from_bytes(&to_array(node_in));
|
||||||
let node_in: Vec<u8> = marshall_from_haskell_var(node, node_len, RW);
|
if sap_note_comm.is_some().into() {
|
||||||
let sap_note_comm = SaplingNoteCommitment::from_bytes(&to_array(node_in));
|
let n = Node::from_cmu(&sap_note_comm.unwrap());
|
||||||
if sap_note_comm.is_some().into() {
|
comm_tree.append(n);
|
||||||
let n = Node::from_cmu(&sap_note_comm.unwrap());
|
let (pos, leaf, omm) = comm_tree.into_parts();
|
||||||
comm_tree.append(n);
|
let f = Hfrontier { position: <u64>::from(pos), leaf: Hhex { bytes: leaf.to_bytes().to_vec()}, ommers: omm.iter().map(|&x| x.to_bytes().to_vec()).collect()};
|
||||||
let mut out_bytes: Vec<u8> = Vec::new();
|
marshall_to_haskell_var(&f, out, out_len, RW);
|
||||||
let result = write_commitment_tree(&comm_tree, &mut out_bytes );
|
} else {
|
||||||
match result {
|
let f0 = Hfrontier { position: 0, leaf: Hhex { bytes: vec![0]}, ommers: vec![vec![0]]};
|
||||||
Ok(()) => {
|
marshall_to_haskell_var(&f0, out, out_len, RW);
|
||||||
let h = Hhex { bytes: out_bytes};
|
|
||||||
marshall_to_haskell_var(&h, out, out_len, RW);
|
|
||||||
},
|
|
||||||
Err(_e) => {
|
|
||||||
let h0 = Hhex { bytes: vec![0]};
|
|
||||||
marshall_to_haskell_var(&h0, out, out_len, RW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let h0 = Hhex { bytes: vec![0]};
|
|
||||||
marshall_to_haskell_var(&h0, out, out_len, RW);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_e) => {
|
|
||||||
let h0 = Hhex { bytes: vec![0]};
|
|
||||||
marshall_to_haskell_var(&h0, out, out_len, RW);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -1420,9 +1427,10 @@ pub extern "C" fn rust_wrapper_read_sapling_witness(
|
||||||
out: *mut u8,
|
out: *mut u8,
|
||||||
out_len: &mut usize
|
out_len: &mut usize
|
||||||
){
|
){
|
||||||
let tree_in: Vec<u8> = marshall_from_haskell_var(tree, tree_len, RW);
|
let tree_in: Hfrontier = marshall_from_haskell_var(tree, tree_len, RW);
|
||||||
let tree_reader = Cursor::new(tree_in);
|
let leaf = Node::from_bytes(to_array(tree_in.leaf.bytes)).unwrap();
|
||||||
let ct: CommitmentTree<Node, SAPLING_DEPTH> = read_commitment_tree(tree_reader).unwrap();
|
let frontier: Frontier<Node, SAPLING_DEPTH> = Frontier::from_parts(Position::from(tree_in.position), leaf, tree_in.ommers.iter().map(|x| Node::from_bytes(to_array(x.clone())).unwrap() ).collect()).unwrap();
|
||||||
|
let ct: CommitmentTree<Node, SAPLING_DEPTH> = CommitmentTree::from_frontier(&frontier);
|
||||||
let inc_wit = IncrementalWitness::from_tree(ct);
|
let inc_wit = IncrementalWitness::from_tree(ct);
|
||||||
let mut out_bytes: Vec<u8> = Vec::new();
|
let mut out_bytes: Vec<u8> = Vec::new();
|
||||||
let result = write_incremental_witness(&inc_wit, &mut out_bytes);
|
let result = write_incremental_witness(&inc_wit, &mut out_bytes);
|
||||||
|
@ -1687,21 +1695,31 @@ pub extern "C" fn rust_wrapper_create_transaction(
|
||||||
build: bool,
|
build: bool,
|
||||||
out: *mut u8,
|
out: *mut u8,
|
||||||
out_len: &mut usize){
|
out_len: &mut usize){
|
||||||
let sap_wit_in: Vec<u8> = marshall_from_haskell_var(sap_wit, sap_wit_len, RW);
|
//let sap_wit_in: Vec<u8> = marshall_from_haskell_var(sap_wit, sap_wit_len, RW);
|
||||||
let sap_wit_reader = Cursor::new(sap_wit_in);
|
//let sap_wit_reader = Cursor::new(sap_wit_in);
|
||||||
let sap_iw = read_commitment_tree::<Node, Cursor<Vec<u8>>, SAPLING_DEPTH>(sap_wit_reader);
|
//let sap_iw = read_commitment_tree::<Node, Cursor<Vec<u8>>, SAPLING_DEPTH>(sap_wit_reader);
|
||||||
let sap_anchor = match sap_iw {
|
let sap_input: Vec<HsaplingInput> = marshall_from_haskell_var(s_input, s_input_len, RW);
|
||||||
Ok(s_iw) => {
|
let sap_anchor =
|
||||||
Some(SaplingAnchor::from(s_iw.root()))
|
if sap_input.is_empty() {
|
||||||
},
|
|
||||||
Err(_e) => {
|
|
||||||
None
|
None
|
||||||
}
|
} else {
|
||||||
};
|
let si = &sap_input[0];
|
||||||
|
let swit_reader = Cursor::new(&si.iw);
|
||||||
|
let iw: IncrementalWitness<Node, SAPLING_DEPTH> = read_incremental_witness(swit_reader).unwrap();
|
||||||
|
Some(SaplingAnchor::from(iw.root()))
|
||||||
|
};
|
||||||
|
//let sap_anchor = match sap_iw {
|
||||||
|
//Ok(s_iw) => {
|
||||||
|
//Some(SaplingAnchor::from(s_iw.root()))
|
||||||
|
//},
|
||||||
|
//Err(_e) => {
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//};
|
||||||
//println!("{:?}", sap_anchor);
|
//println!("{:?}", sap_anchor);
|
||||||
let orch_wit_in: Vec<u8> = marshall_from_haskell_var(orch_wit, orch_wit_len, RW);
|
//let orch_wit_in: Vec<u8> = marshall_from_haskell_var(orch_wit, orch_wit_len, RW);
|
||||||
let orch_wit_reader = Cursor::new(orch_wit_in);
|
//let orch_wit_reader = Cursor::new(orch_wit_in);
|
||||||
let orch_iw = read_commitment_tree::<MerkleHashOrchard, Cursor<Vec<u8>>, 32>(orch_wit_reader);
|
//let orch_iw = read_commitment_tree::<MerkleHashOrchard, Cursor<Vec<u8>>, 32>(orch_wit_reader);
|
||||||
let orch_input: Vec<HorchardInput> = marshall_from_haskell_var(o_input, o_input_len, RW);
|
let orch_input: Vec<HorchardInput> = marshall_from_haskell_var(o_input, o_input_len, RW);
|
||||||
//let orch_anchor = match orch_iw {
|
//let orch_anchor = match orch_iw {
|
||||||
//Ok(o_iw) => {
|
//Ok(o_iw) => {
|
||||||
|
@ -1746,7 +1764,6 @@ pub extern "C" fn rust_wrapper_create_transaction(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sap_input: Vec<HsaplingInput> = marshall_from_haskell_var(s_input, s_input_len, RW);
|
|
||||||
for s_in in sap_input {
|
for s_in in sap_input {
|
||||||
if s_in.sk.len() > 1 {
|
if s_in.sk.len() > 1 {
|
||||||
let sp_key = ExtendedSpendingKey::from_bytes(&s_in.sk);
|
let sp_key = ExtendedSpendingKey::from_bytes(&s_in.sk);
|
||||||
|
@ -1763,13 +1780,32 @@ pub extern "C" fn rust_wrapper_create_transaction(
|
||||||
let iw: IncrementalWitness<Node, SAPLING_DEPTH> = read_incremental_witness(wit_reader).unwrap();
|
let iw: IncrementalWitness<Node, SAPLING_DEPTH> = read_incremental_witness(wit_reader).unwrap();
|
||||||
let merkle_path = iw.path().unwrap();
|
let merkle_path = iw.path().unwrap();
|
||||||
if net {
|
if net {
|
||||||
let _mb = main_builder.add_sapling_spend::<String>(&sk, note, merkle_path).unwrap();
|
let mb = main_builder.add_sapling_spend::<String>(&sk, note, merkle_path);
|
||||||
|
match mb {
|
||||||
|
Ok(()) => {
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
Err(_e) => {
|
||||||
|
let x = Hhex {bytes: vec![5]};
|
||||||
|
marshall_to_haskell_var(&x, out, out_len, RW);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let _tb = test_builder.add_sapling_spend::<String>(&sk, note, merkle_path).unwrap();
|
let tb = test_builder.add_sapling_spend::<String>(&sk, note, merkle_path);
|
||||||
|
match tb {
|
||||||
|
Ok(()) => {
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
Err(_e) => {
|
||||||
|
let x = Hhex {bytes: vec![5]};
|
||||||
|
marshall_to_haskell_var(&x, out, out_len, RW);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
continue;
|
let x = Hhex {bytes: vec![5]};
|
||||||
|
marshall_to_haskell_var(&x, out, out_len, RW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1786,9 +1822,27 @@ pub extern "C" fn rust_wrapper_create_transaction(
|
||||||
let iw: IncrementalWitness<MerkleHashOrchard, 32> = read_incremental_witness(wit_reader).unwrap();
|
let iw: IncrementalWitness<MerkleHashOrchard, 32> = read_incremental_witness(wit_reader).unwrap();
|
||||||
let merkle_path = OrchardMerklePath::from(iw.path().unwrap());
|
let merkle_path = OrchardMerklePath::from(iw.path().unwrap());
|
||||||
if net {
|
if net {
|
||||||
let _mb = main_builder.add_orchard_spend::<String>(&sp_key, note, merkle_path).unwrap();
|
let mb = main_builder.add_orchard_spend::<String>(&sp_key, note, merkle_path);
|
||||||
|
match mb {
|
||||||
|
Ok(()) => {
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
Err(_e) => {
|
||||||
|
let x = Hhex {bytes: vec![7]};
|
||||||
|
marshall_to_haskell_var(&x, out, out_len, RW);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let _tb = test_builder.add_orchard_spend::<String>(&sp_key, note, merkle_path).unwrap();
|
let tb = test_builder.add_orchard_spend::<String>(&sp_key, note, merkle_path);
|
||||||
|
match tb {
|
||||||
|
Ok(()) => {
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
Err(_e) => {
|
||||||
|
let x = Hhex {bytes: vec![7]};
|
||||||
|
marshall_to_haskell_var(&x, out, out_len, RW);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,15 +204,15 @@ import ZcashHaskell.Types
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{# fun unsafe rust_wrapper_read_sapling_commitment_tree as rustWrapperReadSaplingCommitmentTree
|
{# fun unsafe rust_wrapper_read_sapling_commitment_tree as rustWrapperReadSaplingCommitmentTree
|
||||||
{ toBorshVar* `BS.ByteString'&
|
{ toBorshVar* `SaplingFrontier'&
|
||||||
, toBorshVar* `BS.ByteString'&
|
, toBorshVar* `BS.ByteString'&
|
||||||
, getVarBuffer `Buffer HexString'&
|
, getVarBuffer `Buffer SaplingFrontier'&
|
||||||
}
|
}
|
||||||
-> `()'
|
-> `()'
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{# fun unsafe rust_wrapper_read_sapling_witness as rustWrapperReadSaplingWitness
|
{# fun unsafe rust_wrapper_read_sapling_witness as rustWrapperReadSaplingWitness
|
||||||
{ toBorshVar* `BS.ByteString'&
|
{ toBorshVar* `SaplingFrontier'&
|
||||||
, getVarBuffer `Buffer HexString'&
|
, getVarBuffer `Buffer HexString'&
|
||||||
}
|
}
|
||||||
-> `()'
|
-> `()'
|
||||||
|
@ -232,6 +232,13 @@ import ZcashHaskell.Types
|
||||||
-> `()'
|
-> `()'
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
{# fun unsafe rust_wrapper_read_sapling_frontier as rustWrapperReadSaplingFrontier
|
||||||
|
{ toBorshVar* `BS.ByteString'&
|
||||||
|
, getVarBuffer `Buffer SaplingFrontier'&
|
||||||
|
}
|
||||||
|
-> `()'
|
||||||
|
#}
|
||||||
|
|
||||||
{# fun unsafe rust_wrapper_decode_sapling_address as rustWrapperDecodeSaplingAddress
|
{# fun unsafe rust_wrapper_decode_sapling_address as rustWrapperDecodeSaplingAddress
|
||||||
{ toBorshVar* `BS.ByteString'&
|
{ toBorshVar* `BS.ByteString'&
|
||||||
, getVarBuffer `Buffer (BS.ByteString)'&
|
, getVarBuffer `Buffer (BS.ByteString)'&
|
||||||
|
|
|
@ -21,6 +21,7 @@ import C.Zcash
|
||||||
( rustWrapperDecodeSaplingAddress
|
( rustWrapperDecodeSaplingAddress
|
||||||
, rustWrapperIsShielded
|
, rustWrapperIsShielded
|
||||||
, rustWrapperReadSaplingCommitmentTree
|
, rustWrapperReadSaplingCommitmentTree
|
||||||
|
, rustWrapperReadSaplingFrontier
|
||||||
, rustWrapperReadSaplingPosition
|
, rustWrapperReadSaplingPosition
|
||||||
, rustWrapperReadSaplingWitness
|
, rustWrapperReadSaplingWitness
|
||||||
, rustWrapperSaplingCheck
|
, rustWrapperSaplingCheck
|
||||||
|
@ -184,32 +185,38 @@ genSaplingInternalAddress sk =
|
||||||
res =
|
res =
|
||||||
withPureBorshVarBuffer (rustWrapperSaplingChgPaymentAddress $ getBytes sk)
|
withPureBorshVarBuffer (rustWrapperSaplingChgPaymentAddress $ getBytes sk)
|
||||||
|
|
||||||
-- | Update a Sapling commitment tree
|
getSaplingFrontier :: SaplingCommitmentTree -> Maybe SaplingFrontier
|
||||||
updateSaplingCommitmentTree ::
|
getSaplingFrontier tree =
|
||||||
SaplingCommitmentTree -- ^ the base tree
|
if sf_pos updatedTree > 1
|
||||||
-> HexString -- ^ the new note commitment
|
then Just updatedTree
|
||||||
-> Maybe SaplingCommitmentTree
|
|
||||||
updateSaplingCommitmentTree tree cmu =
|
|
||||||
if BS.length (hexBytes updatedTree) > 1
|
|
||||||
then Just $ SaplingCommitmentTree updatedTree
|
|
||||||
else Nothing
|
else Nothing
|
||||||
where
|
where
|
||||||
updatedTree =
|
updatedTree =
|
||||||
withPureBorshVarBuffer $
|
withPureBorshVarBuffer $
|
||||||
rustWrapperReadSaplingCommitmentTree
|
rustWrapperReadSaplingFrontier $ toBytes $ sapTree tree
|
||||||
(hexBytes $ sapTree tree)
|
|
||||||
(hexBytes cmu)
|
-- | Update a Sapling commitment tree
|
||||||
|
updateSaplingCommitmentTree ::
|
||||||
|
SaplingFrontier -- ^ the base tree
|
||||||
|
-> HexString -- ^ the new note commitment
|
||||||
|
-> Maybe SaplingFrontier
|
||||||
|
updateSaplingCommitmentTree tree cmu =
|
||||||
|
if sf_pos updatedTree > 1
|
||||||
|
then Just updatedTree
|
||||||
|
else Nothing
|
||||||
|
where
|
||||||
|
updatedTree =
|
||||||
|
withPureBorshVarBuffer $
|
||||||
|
rustWrapperReadSaplingCommitmentTree tree (hexBytes cmu)
|
||||||
|
|
||||||
-- | Get the Sapling incremental witness from a commitment tree
|
-- | Get the Sapling incremental witness from a commitment tree
|
||||||
getSaplingWitness :: SaplingCommitmentTree -> Maybe SaplingWitness
|
getSaplingWitness :: SaplingFrontier -> Maybe SaplingWitness
|
||||||
getSaplingWitness tree =
|
getSaplingWitness tree =
|
||||||
if BS.length (hexBytes wit) > 1
|
if BS.length (hexBytes wit) > 1
|
||||||
then Just $ SaplingWitness wit
|
then Just $ SaplingWitness wit
|
||||||
else Nothing
|
else Nothing
|
||||||
where
|
where
|
||||||
wit =
|
wit = withPureBorshVarBuffer $ rustWrapperReadSaplingWitness tree
|
||||||
withPureBorshVarBuffer $
|
|
||||||
rustWrapperReadSaplingWitness (hexBytes $ sapTree tree)
|
|
||||||
|
|
||||||
-- | Get the Sapling note position from a witness
|
-- | Get the Sapling note position from a witness
|
||||||
getSaplingNotePosition :: SaplingWitness -> Integer
|
getSaplingNotePosition :: SaplingWitness -> Integer
|
||||||
|
|
|
@ -611,6 +611,15 @@ newtype SaplingCommitmentTree = SaplingCommitmentTree
|
||||||
{ sapTree :: HexString
|
{ sapTree :: HexString
|
||||||
} deriving (Eq, Prelude.Show, Read)
|
} deriving (Eq, Prelude.Show, Read)
|
||||||
|
|
||||||
|
data SaplingFrontier = SaplingFrontier
|
||||||
|
{ sf_pos :: !Int64
|
||||||
|
, sf_leaf :: !HexString
|
||||||
|
, sf_ommers :: ![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 SaplingFrontier
|
||||||
|
|
||||||
-- | Type for a Sapling incremental witness
|
-- | Type for a Sapling incremental witness
|
||||||
newtype SaplingWitness = SaplingWitness
|
newtype SaplingWitness = SaplingWitness
|
||||||
{ sapWit :: HexString
|
{ sapWit :: HexString
|
||||||
|
|
|
@ -5,7 +5,7 @@ cabal-version: 3.0
|
||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: zcash-haskell
|
name: zcash-haskell
|
||||||
version: 0.7.1.1
|
version: 0.7.2.0
|
||||||
synopsis: Utilities to interact with the Zcash blockchain
|
synopsis: Utilities to interact with the Zcash blockchain
|
||||||
description: Please see the README on the repo at <https://git.vergara.tech/Vergara_Tech/zcash-haskell#readme>
|
description: Please see the README on the repo at <https://git.vergara.tech/Vergara_Tech/zcash-haskell#readme>
|
||||||
category: Blockchain
|
category: Blockchain
|
||||||
|
|
Loading…
Reference in a new issue