Repository: ergoplatform/sigma-rust (language: rust)

Issue:
[300 SigmaUSD] Example SPV workflow with NiPoPoW
Implement in JS/TS the workflow described in #495 for a chosen payment on mainnet. Pick any payment(box) on the mainnet.
This example aims to show the developers how they can check that a payment was made using SPV API with NiPoPoW.
Ideally, we'd want that workflow implemented as a test against hardcoded nodes. 



Discussion / hints:
I will work on this.

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/block_header.rs b/bindings/ergo-lib-c-core/src/block_header.rs
index ddfdf19fc..371b5f50b 100644
--- a/bindings/ergo-lib-c-core/src/block_header.rs
+++ b/bindings/ergo-lib-c-core/src/block_header.rs
@@ -34,6 +34,18 @@ pub unsafe fn block_header_id(
     Ok(())
 }
 
+/// Copy the contents of `transactions_root` field to `output`. Key assumption: exactly 32 bytes of
+/// memory have been allocated at the address pointed-to by `output`.
+pub unsafe fn block_header_transactions_root(
+    block_header_ptr: ConstBlockHeaderPtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let block_header = const_ptr_as_ref(block_header_ptr, "block_header_ptr")?;
+    let src = Vec::<u8>::from(block_header.0.transaction_root.clone());
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, src.len());
+    Ok(())
+}
+
 /// Block id
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct BlockId(pub(crate) ergo_lib::ergo_chain_types::BlockId);
diff --git a/bindings/ergo-lib-c-core/src/rest/api/node.rs b/bindings/ergo-lib-c-core/src/rest/api/node.rs
index 6ffbe207e..7cd7cf121 100644
--- a/bindings/ergo-lib-c-core/src/rest/api/node.rs
+++ b/bindings/ergo-lib-c-core/src/rest/api/node.rs
@@ -4,11 +4,11 @@ use std::ffi::{CStr, CString};
 use std::os::raw::c_char;
 use std::str::FromStr;
 
-use crate::block_header::ConstBlockIdPtr;
 use crate::rest::c_string_collection::{CStringCollection, CStringCollectionInner};
 use crate::rest::node_conf::NodeConfPtr;
 use crate::util::const_ptr_as_ref;
 use crate::Error;
+use crate::{block_header::ConstBlockIdPtr, transaction::ConstTxIdPtr};
 
 use self::abortable::spawn_abortable;
 
@@ -41,6 +41,60 @@ pub unsafe fn rest_api_node_get_info(
     Ok(())
 }
 
+/// GET on /blocks/{blockId}/header endpoint
+pub unsafe fn rest_api_node_get_header(
+    runtime_ptr: RestApiRuntimePtr,
+    node_conf_ptr: NodeConfPtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    header_id_ptr: ConstBlockIdPtr,
+) -> Result<(), Error> {
+    let runtime = const_ptr_as_ref(runtime_ptr, "runtime_ptr")?;
+    let node_conf = const_ptr_as_ref(node_conf_ptr, "node_conf_ptr")?.0;
+    let header_id = const_ptr_as_ref(header_id_ptr, "header_id_ptr")?.0.clone();
+    let abort_callback: AbortCallback = (&callback).into();
+    let abort_handle = spawn_abortable(runtime, async move {
+        match ergo_lib::ergo_rest::api::node::get_header(node_conf, header_id).await {
+            Ok(header) => callback.succeeded(header),
+            Err(e) => callback.failed(e.into()),
+        }
+    })?;
+    let request_handle = RequestHandle::new(abort_handle, abort_callback);
+    *request_handle_out = Box::into_raw(Box::new(request_handle));
+    Ok(())
+}
+
+/// GET on /blocks/{header_id}/proofFor/{tx_id} to request the merkle proof for a given transaction
+/// that belongs to the given header ID.
+pub unsafe fn rest_api_node_get_blocks_header_id_proof_for_tx_id(
+    runtime_ptr: RestApiRuntimePtr,
+    node_conf_ptr: NodeConfPtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    header_id_ptr: ConstBlockIdPtr,
+    tx_id_ptr: ConstTxIdPtr,
+) -> Result<(), Error> {
+    let runtime = const_ptr_as_ref(runtime_ptr, "runtime_ptr")?;
+    let node_conf = const_ptr_as_ref(node_conf_ptr, "node_conf_ptr")?.0;
+    let header_id = const_ptr_as_ref(header_id_ptr, "header_id_ptr")?.0.clone();
+    let tx_id = const_ptr_as_ref(tx_id_ptr, "tx_id_ptr")?.0.clone();
+    let abort_callback: AbortCallback = (&callback).into();
+    let abort_handle = spawn_abortable(runtime, async move {
+        match ergo_lib::ergo_rest::api::node::get_blocks_header_id_proof_for_tx_id(
+            node_conf, header_id, tx_id,
+        )
+        .await
+        {
+            Ok(Some(merkle_proof)) => callback.succeeded(merkle_proof),
+            Ok(None) => callback.failed(Error::Misc("No merkle proof".into())),
+            Err(e) => callback.failed(e.into()),
+        }
+    })?;
+    let request_handle = RequestHandle::new(abort_handle, abort_callback);
+    *request_handle_out = Box::into_raw(Box::new(request_handle));
+    Ok(())
+}
+
 /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
 pub unsafe fn rest_api_node_get_nipopow_proof_by_header_id(
     runtime_ptr: RestApiRuntimePtr,
diff --git a/bindings/ergo-lib-c-core/src/rest/node_info.rs b/bindings/ergo-lib-c-core/src/rest/node_info.rs
index f5b2d309c..a3304b096 100644
--- a/bindings/ergo-lib-c-core/src/rest/node_info.rs
+++ b/bindings/ergo-lib-c-core/src/rest/node_info.rs
@@ -1,4 +1,4 @@
-use crate::util::const_ptr_as_ref;
+use crate::{error::Error, util::const_ptr_as_ref};
 
 #[derive(derive_more::From, derive_more::Into)]
 pub struct NodeInfo(pub(crate) ergo_lib::ergo_rest::NodeInfo);
@@ -9,3 +9,12 @@ pub unsafe fn node_info_get_name(node_info_ptr: NodeInfoPtr) -> String {
     let node_info = const_ptr_as_ref(node_info_ptr, "node_info_ptr").unwrap();
     node_info.0.name.clone()
 }
+
+/// Returns true iff the ergo node is at least v4.0.28. This is important since nipopow proofs only
+/// work correctly from this version onwards.
+pub unsafe fn node_info_is_at_least_version_4_0_28(
+    node_info_ptr: NodeInfoPtr,
+) -> Result<bool, Error> {
+    let node_info = const_ptr_as_ref(node_info_ptr, "node_info_ptr")?;
+    Ok(node_info.0.is_at_least_version_4_0_28())
+}
diff --git a/bindings/ergo-lib-c/src/block_header.rs b/bindings/ergo-lib-c/src/block_header.rs
index 0cb955c29..13d2428f8 100644
--- a/bindings/ergo-lib-c/src/block_header.rs
+++ b/bindings/ergo-lib-c/src/block_header.rs
@@ -3,8 +3,8 @@ use paste::paste;
 
 use ergo_lib_c_core::{
     block_header::{
-        block_header_from_json, block_header_id, BlockHeader, BlockHeaderPtr, BlockId, BlockIdPtr,
-        ConstBlockHeaderPtr, ConstBlockIdPtr,
+        block_header_from_json, block_header_id, block_header_transactions_root, BlockHeader,
+        BlockHeaderPtr, BlockId, BlockIdPtr, ConstBlockHeaderPtr, ConstBlockIdPtr,
     },
     Error,
 };
@@ -33,6 +33,17 @@ pub unsafe extern "C" fn ergo_lib_block_header_id(
     block_header_id(block_header_ptr, block_id_out).unwrap();
 }
 
+/// Copy the contents of `transactions_root` field to `output`. Key assumption: exactly 32 bytes of
+/// memory have been allocated at the address pointed-to by `output`.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_block_header_transactions_root(
+    block_header_ptr: ConstBlockHeaderPtr,
+    output: *mut u8,
+) -> ErrorPtr {
+    let res = block_header_transactions_root(block_header_ptr, output);
+    Error::c_api_from(res)
+}
+
 /// Delete `BlockHeader`
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_block_header_delete(ptr: BlockHeaderPtr) {
diff --git a/bindings/ergo-lib-c/src/rest/api/node.rs b/bindings/ergo-lib-c/src/rest/api/node.rs
index 483536fc8..6b2860982 100644
--- a/bindings/ergo-lib-c/src/rest/api/node.rs
+++ b/bindings/ergo-lib-c/src/rest/api/node.rs
@@ -1,12 +1,14 @@
 use ergo_lib_c_core::block_header::ConstBlockIdPtr;
 use ergo_lib_c_core::rest::api::callback::CompletionCallback;
 use ergo_lib_c_core::rest::api::node::{
+    rest_api_node_get_blocks_header_id_proof_for_tx_id, rest_api_node_get_header,
     rest_api_node_get_info, rest_api_node_get_nipopow_proof_by_header_id,
     rest_api_node_peer_discovery,
 };
 use ergo_lib_c_core::rest::api::request_handle::RequestHandlePtr;
 use ergo_lib_c_core::rest::api::runtime::RestApiRuntimePtr;
 use ergo_lib_c_core::rest::node_conf::NodeConfPtr;
+use ergo_lib_c_core::transaction::ConstTxIdPtr;
 use ergo_lib_c_core::Error;
 use ergo_lib_c_core::ErrorPtr;
 use std::os::raw::c_char;
@@ -23,6 +25,47 @@ pub unsafe extern "C" fn ergo_lib_rest_api_node_get_info(
     Error::c_api_from(res)
 }
 
+/// GET on /blocks/{blockId}/header endpoint
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_rest_api_node_get_header(
+    runtime_ptr: RestApiRuntimePtr,
+    node_conf_ptr: NodeConfPtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    header_id_ptr: ConstBlockIdPtr,
+) -> ErrorPtr {
+    let res = rest_api_node_get_header(
+        runtime_ptr,
+        node_conf_ptr,
+        callback,
+        request_handle_out,
+        header_id_ptr,
+    );
+    Error::c_api_from(res)
+}
+
+/// GET on /blocks/{header_id}/proofFor/{tx_id} to request the merkle proof for a given transaction
+/// that belongs to the given header ID.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_rest_api_node_get_blocks_header_id_proof_for_tx_id(
+    runtime_ptr: RestApiRuntimePtr,
+    node_conf_ptr: NodeConfPtr,
+    callback: CompletionCallback,
+    request_handle_out: *mut RequestHandlePtr,
+    header_id_ptr: ConstBlockIdPtr,
+    tx_id_ptr: ConstTxIdPtr,
+) -> ErrorPtr {
+    let res = rest_api_node_get_blocks_header_id_proof_for_tx_id(
+        runtime_ptr,
+        node_conf_ptr,
+        callback,
+        request_handle_out,
+        header_id_ptr,
+        tx_id_ptr,
+    );
+    Error::c_api_from(res)
+}
+
 /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_rest_api_node_get_nipopow_proof_by_header_id(
diff --git a/bindings/ergo-lib-c/src/rest/node_info.rs b/bindings/ergo-lib-c/src/rest/node_info.rs
index 258325dd0..b51fe465a 100644
--- a/bindings/ergo-lib-c/src/rest/node_info.rs
+++ b/bindings/ergo-lib-c/src/rest/node_info.rs
@@ -1,8 +1,8 @@
 use std::ffi::CString;
 use std::os::raw::c_char;
 
-use ergo_lib_c_core::rest::node_info::node_info_get_name;
 use ergo_lib_c_core::rest::node_info::NodeInfoPtr;
+use ergo_lib_c_core::rest::node_info::{node_info_get_name, node_info_is_at_least_version_4_0_28};
 
 use crate::delete_ptr;
 
@@ -22,3 +22,14 @@ pub unsafe extern "C" fn ergo_lib_node_info_get_name(
     let s = node_info_get_name(ptr);
     *name_str = CString::new(s).unwrap().into_raw();
 }
+
+/// Returns true iff the ergo node is at least v4.0.28. This is important since nipopow proofs only
+/// work correctly from this version onwards.
+#[allow(clippy::unwrap_used)]
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_node_info_is_at_least_version_4_0_28(
+    node_info_ptr: NodeInfoPtr,
+) -> bool {
+    #[allow(clippy::unwrap_used)]
+    node_info_is_at_least_version_4_0_28(node_info_ptr).unwrap()
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
index 88cc87657..d529644b8 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
@@ -2,7 +2,7 @@ import Foundation
 import ErgoLibC
 
 /// Represents data of the block header available in Sigma propositions.
-class BlockHeader {
+class BlockHeader: FromRawPtr {
     internal var pointer: BlockHeaderPtr
     
     /// Parse BlockHeader array from JSON (Node API)
@@ -21,6 +21,10 @@ class BlockHeader {
         self.pointer = ptr
     }
     
+    static func fromRawPtr(ptr: UnsafeRawPointer) -> Self {
+        return BlockHeader(withRawPointer: OpaquePointer(ptr)) as! Self
+    }
+
     /// Get Block id
     func getBlockId() -> BlockId {
         var ptr: BlockIdPtr?
@@ -28,6 +32,14 @@ class BlockHeader {
         return BlockId(withRawPointer: ptr!)
     }
     
+    /// Get transactions root field of the header
+    func getTransactionsRoot() throws -> [UInt8] {
+        var bytes = Array.init(repeating: UInt8(0), count: 32)
+        let error = ergo_lib_block_header_transactions_root(self.pointer, &bytes)
+        try checkError(error)
+        return bytes
+    }
+
     deinit {
         ergo_lib_block_header_delete(self.pointer)
     }
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/MerkleProof.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/MerkleProof.swift
index b7d9f2697..79fb64bac 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/MerkleProof.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/MerkleProof.swift
@@ -6,7 +6,7 @@ enum NodeSide: UInt8 {
     case Left = 0
     case Right = 1
 }
-class MerkleProof {
+class MerkleProof: FromRawPtr {
     internal var pointer: MerkleProofPtr
 
     init(withJson json: String) throws {
@@ -17,6 +17,7 @@ class MerkleProof {
         try checkError(error)
         self.pointer = ptr!
     }
+
     init(leafData: [UInt8]) throws {
         var ptr: MerkleProofPtr?
         let error = ergo_merkle_proof_new(leafData, UInt(leafData.count), &ptr)
@@ -24,6 +25,16 @@ class MerkleProof {
         self.pointer = ptr!
     }
 
+    /// Takes ownership of an existing ``MerkleProofPtr``. Note: we must ensure that no other instance
+    /// of ``MerkleProof`` can hold this pointer.
+    internal init(withRawPointer ptr: MerkleProofPtr) {
+        self.pointer = ptr
+    }
+
+    static func fromRawPtr(ptr: UnsafeRawPointer) -> Self {
+        return MerkleProof(withRawPointer: OpaquePointer(ptr)) as! Self
+    }
+
     /// Adds a new node and it's hash to the MerkleProof. Hash must be 32 bytes in size
     func addNode(hash: [UInt8], side: NodeSide) throws {
         let error = ergo_merkle_proof_add_node(self.pointer, hash, UInt(hash.count), side.rawValue)
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/NodeInfo.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/NodeInfo.swift
index 938e7428e..34493e516 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/NodeInfo.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/NodeInfo.swift
@@ -20,6 +20,10 @@ class NodeInfo: FromRawPtr {
         return str
     }
 
+    func isAtLeastVersion4028() -> Bool {
+        return ergo_lib_node_info_is_at_least_version_4_0_28(self.pointer)
+    }
+
     deinit {
         ergo_lib_node_info_delete(self.pointer)
     }
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift
index 04db5c463..cf13e632e 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/RestNodeApi.swift
@@ -42,7 +42,73 @@ class RestNodeApi {
             } catch {}
         }
     }
-    
+
+    /// GET on /blocks/{blockId}/header endpoint
+    func getHeader(
+        nodeConf: NodeConf,
+        blockId: BlockId,
+        closure: @escaping (Result<BlockHeader, Error>) -> Void
+    ) throws -> RequestHandle {
+        let completion = wrapClosure(closure)
+        var requestHandlerPtr: RequestHandlePtr?
+        let error = ergo_lib_rest_api_node_get_header(
+            self.pointer,
+            nodeConf.pointer,
+            completion,
+            &requestHandlerPtr,
+            blockId.pointer
+        )
+        try checkError(error)
+        return RequestHandle(withRawPtr: requestHandlerPtr!)
+    }
+
+    /// GET on /blocks/{blockId}/header endpoint (async)
+    @available(macOS 10.15, iOS 13, *)
+    func getHeaderAsync(nodeConf: NodeConf, blockId: BlockId) async throws -> BlockHeader {
+        try await withCheckedThrowingContinuation { continuation in
+            do {
+                let _ = try getHeader(nodeConf: nodeConf, blockId: blockId) { result in
+                    continuation.resume(with: result)
+                }
+            } catch {}
+        }
+    }
+
+    /// GET on /blocks/{header_id}/proofFor/{tx_id} to request the merkle proof for a given transaction
+    /// that belongs to the given header ID.
+    func getBlocksHeaderIdProofForTxId(
+        nodeConf: NodeConf,
+        blockId: BlockId,
+        txId: TxId,
+        closure: @escaping (Result<MerkleProof, Error>) -> Void
+    ) throws -> RequestHandle {
+        let completion = wrapClosure(closure)
+        var requestHandlerPtr: RequestHandlePtr?
+        let error = ergo_lib_rest_api_node_get_blocks_header_id_proof_for_tx_id(
+            self.pointer,
+            nodeConf.pointer,
+            completion,
+            &requestHandlerPtr,
+            blockId.pointer,
+            txId.pointer
+        )
+        try checkError(error)
+        return RequestHandle(withRawPtr: requestHandlerPtr!)
+    }
+
+    /// GET on /blocks/{header_id}/proofFor/{tx_id} to request the merkle proof for a given transaction
+    /// that belongs to the given header ID (async).
+    @available(macOS 10.15, iOS 13, *)
+    func getBlocksHeaderIdProofForTxIdAsync(nodeConf: NodeConf, blockId: BlockId, txId: TxId) async throws -> MerkleProof {
+        try await withCheckedThrowingContinuation { continuation in
+            do {
+                let _ = try getBlocksHeaderIdProofForTxId(nodeConf: nodeConf, blockId: blockId, txId: txId) { result in
+                    continuation.resume(with: result)
+                }
+            } catch {}
+        }
+    }
+
     /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
     func getNipopowProofByHeaderId(
         nodeConf: NodeConf,
diff --git a/bindings/ergo-lib-wasm/Cargo.toml b/bindings/ergo-lib-wasm/Cargo.toml
index cdb1fad31..8e1dbdc23 100644
--- a/bindings/ergo-lib-wasm/Cargo.toml
+++ b/bindings/ergo-lib-wasm/Cargo.toml
@@ -23,6 +23,7 @@ js-sys = "0.3"
 web-sys = {version = "0.3", features = ["Url"]}
 url = "2.2"
 bounded-integer = { version = "^0.5", features = ["types"] }
+futures = "0.3"
 
 # used in elliptic-curve(in ergo-lib), compiled here with WASM support
 getrandom = {version = "0.2.3", features = ["js"]}
diff --git a/bindings/ergo-lib-wasm/karma.conf.js b/bindings/ergo-lib-wasm/karma.conf.js
index d98028c9e..9e0370ba1 100644
--- a/bindings/ergo-lib-wasm/karma.conf.js
+++ b/bindings/ergo-lib-wasm/karma.conf.js
@@ -58,6 +58,7 @@ module.exports = function (config) {
     logLevel: config.LOG_INFO,
 
     browsers: ["ChromeHeadless"],
+    browserNoActivityTimeout: 60000,
 
     autoWatch: false,
     singleRun: true,
diff --git a/bindings/ergo-lib-wasm/src/block_header.rs b/bindings/ergo-lib-wasm/src/block_header.rs
index 528417d33..84990b4f0 100644
--- a/bindings/ergo-lib-wasm/src/block_header.rs
+++ b/bindings/ergo-lib-wasm/src/block_header.rs
@@ -26,6 +26,11 @@ impl BlockHeader {
     pub fn id(&self) -> BlockId {
         self.0.id.clone().into()
     }
+
+    /// Get transactions root
+    pub fn transactions_root(&self) -> Vec<u8> {
+        self.0.transaction_root.clone().into()
+    }
 }
 
 /// Block id
@@ -33,10 +38,26 @@ impl BlockHeader {
 #[derive(PartialEq, Eq, Debug, Clone, From, Into)]
 pub struct BlockId(pub(crate) ergo_lib::ergo_chain_types::BlockId);
 
+#[wasm_bindgen]
+impl BlockId {
+    /// Parse from base 16 encoded string
+    #[allow(clippy::should_implement_trait)]
+    pub fn from_str(id: &str) -> Result<BlockId, JsValue> {
+        ergo_lib::ergo_chain_types::Digest32::try_from(String::from(id))
+            .map(|d| BlockId(ergo_lib::ergo_chain_types::BlockId(d)))
+            .map_err(to_js)
+    }
+
+    /// Equality check
+    pub fn equals(&self, id: &BlockId) -> bool {
+        self.0 == id.0
+    }
+}
+
 /// Collection of BlockHeaders
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone)]
-pub struct BlockHeaders(Vec<BlockHeader>);
+pub struct BlockHeaders(pub(crate) Vec<BlockHeader>);
 
 #[wasm_bindgen]
 impl BlockHeaders {
diff --git a/bindings/ergo-lib-wasm/src/merkleproof.rs b/bindings/ergo-lib-wasm/src/merkleproof.rs
index 618b9fe18..1e1f3ea19 100644
--- a/bindings/ergo-lib-wasm/src/merkleproof.rs
+++ b/bindings/ergo-lib-wasm/src/merkleproof.rs
@@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*;
 
 #[wasm_bindgen]
 /// A MerkleProof type. Given leaf data and levels (bottom-upwards), the root hash can be computed and validated
-pub struct MerkleProof(ergo_merkle_tree::MerkleProof);
+pub struct MerkleProof(pub(crate) ergo_merkle_tree::MerkleProof);
 
 /// A level node in a merkle proof
 #[wasm_bindgen]
diff --git a/bindings/ergo-lib-wasm/src/nipopow.rs b/bindings/ergo-lib-wasm/src/nipopow.rs
index 729c74cf7..09b3d0526 100644
--- a/bindings/ergo-lib-wasm/src/nipopow.rs
+++ b/bindings/ergo-lib-wasm/src/nipopow.rs
@@ -4,13 +4,17 @@ use super::block_header::BlockId;
 use derive_more::{From, Into};
 use wasm_bindgen::prelude::*;
 
-use crate::{block_header::BlockHeader, error_conversion::to_js};
+use crate::{
+    block_header::{BlockHeader, BlockHeaders},
+    error_conversion::to_js,
+};
 
 /// A structure representing NiPoPow proof.
 #[wasm_bindgen]
 #[derive(Debug, From, Into)]
 pub struct NipopowProof(ergo_lib::ergo_nipopow::NipopowProof);
 
+#[wasm_bindgen]
 impl NipopowProof {
     /// Implementation of the ≥ algorithm from [`KMZ17`], see Algorithm 4
     ///
@@ -24,9 +28,14 @@ impl NipopowProof {
         serde_json::to_string_pretty(&self.0).map_err(to_js)
     }
 
+    /// Get suffix head
+    pub fn suffix_head(&self) -> PoPowHeader {
+        self.0.suffix_head.clone().into()
+    }
+
     /// Parse from JSON
     /// supports Ergo Node/Explorer API and box values and token amount encoded as strings
-    pub fn from_json(json: &str) -> Result<Self, JsValue> {
+    pub fn from_json(json: &str) -> Result<NipopowProof, JsValue> {
         serde_json::from_str(json).map(Self).map_err(to_js)
     }
 }
@@ -37,15 +46,22 @@ impl NipopowProof {
 #[derive(Debug, From, Into)]
 pub struct NipopowVerifier(ergo_lib::ergo_nipopow::NipopowVerifier);
 
+#[wasm_bindgen]
 impl NipopowVerifier {
     /// Create new instance
-    pub fn new(genesis_block_id: BlockId) -> Self {
+    #[wasm_bindgen(constructor)]
+    pub fn new(genesis_block_id: BlockId) -> NipopowVerifier {
         ergo_lib::ergo_nipopow::NipopowVerifier::new(genesis_block_id.0).into()
     }
 
+    /// Return best proof
+    pub fn best_proof(&self) -> Option<NipopowProof> {
+        self.0.best_proof().map(NipopowProof)
+    }
+
     /// Returns chain of `BlockHeader`s from the best proof.
-    pub fn best_chain(&self) -> Vec<BlockHeader> {
-        self.0.best_chain().into_iter().map(|h| h.into()).collect()
+    pub fn best_chain(&self) -> BlockHeaders {
+        BlockHeaders(self.0.best_chain().into_iter().map(|h| h.into()).collect())
     }
 
     /// Process given proof
diff --git a/bindings/ergo-lib-wasm/src/rest.rs b/bindings/ergo-lib-wasm/src/rest.rs
index b088fe0c5..30f62daf2 100644
--- a/bindings/ergo-lib-wasm/src/rest.rs
+++ b/bindings/ergo-lib-wasm/src/rest.rs
@@ -2,3 +2,4 @@
 
 pub mod api;
 pub mod node_conf;
+pub mod node_info;
diff --git a/bindings/ergo-lib-wasm/src/rest/api.rs b/bindings/ergo-lib-wasm/src/rest/api.rs
index b6e491481..468eb7ccb 100644
--- a/bindings/ergo-lib-wasm/src/rest/api.rs
+++ b/bindings/ergo-lib-wasm/src/rest/api.rs
@@ -1,39 +1,106 @@
 //! Wasm API for ergo_rest::api
+//!
+//! Note that all the functions for GET requests are not async and furthermore return an instannce
+//! of `js_sys::Promise`. The reason is some of the args are passed in by reference, which is a
+//! problem since futures need to have a 'static lifetime. The workaround is to clone the args and
+//! pass it into an `async move` block, and convert that into a JS promise directly (described in
+//! https://github.com/rustwasm/wasm-bindgen/issues/1858).
 
 use wasm_bindgen::prelude::*;
 
 use super::node_conf::NodeConf;
-use crate::{block_header::BlockId, error_conversion::to_js, nipopow::NipopowProof};
+use crate::{
+    block_header::{BlockHeader, BlockId},
+    error_conversion::to_js,
+    nipopow::NipopowProof,
+    transaction::TxId,
+};
 use bounded_vec::NonEmptyVec;
 use std::time::Duration;
 
 #[wasm_bindgen]
 /// GET on /info endpoint
-pub async fn get_info(node: NodeConf) -> Result<JsValue, JsValue> {
-    // TODO: check if node is not null in JS after the call (because it implements Copy)
-    ergo_lib::ergo_rest::api::node::get_info(node.into())
-        .await
-        .map_err(to_js)
-        .map(|info| JsValue::from_str(&info.name))
+pub fn get_info(node: &NodeConf) -> js_sys::Promise {
+    // Note that we can't pass in `node` by value as it will fail on the JS side if used again,
+    // despite the Copy implementation.  The problem is a bit mysterious; after calling this
+    // function, `node` isn't null on the JS side but when used again it will crash on the rust side
+    // (it complains of a null pointer there).
+    //
+    // A related issue is here: https://github.com/rustwasm/wasm-bindgen/issues/2204
+    #[allow(clippy::clone_on_copy)]
+    let node_cloned = node.0.clone();
+    wasm_bindgen_futures::future_to_promise(async move {
+        let info = ergo_lib::ergo_rest::api::node::get_info(node_cloned)
+            .await
+            .map_err(to_js)
+            .map(super::node_info::NodeInfo::from)?;
+        Ok(wasm_bindgen::JsValue::from(info))
+    })
+}
+
+#[wasm_bindgen]
+/// GET on /blocks/{header_id}/header endpoint
+pub fn get_header(node: &NodeConf, header_id: &BlockId) -> js_sys::Promise {
+    let header_id_cloned = header_id.0.clone();
+    #[allow(clippy::clone_on_copy)]
+    let node_cloned = node.0.clone();
+    wasm_bindgen_futures::future_to_promise(async move {
+        let header = ergo_lib::ergo_rest::api::node::get_header(node_cloned, header_id_cloned)
+            .await
+            .map_err(to_js)
+            .map(BlockHeader::from)?;
+        Ok(wasm_bindgen::JsValue::from(header))
+    })
 }
 
 #[wasm_bindgen]
 /// GET on /nipopow/proof/{minChainLength}/{suffixLength}/{headerId} endpoint
-pub async fn get_nipopow_proof_by_header_id(
-    node: NodeConf,
+pub fn get_nipopow_proof_by_header_id(
+    node: &NodeConf,
     min_chain_length: u32,
     suffix_len: u32,
-    header_id: BlockId,
-) -> Result<NipopowProof, JsValue> {
-    ergo_lib::ergo_rest::api::node::get_nipopow_proof_by_header_id(
-        node.into(),
-        min_chain_length,
-        suffix_len,
-        header_id.into(),
-    )
-    .await
-    .map_err(to_js)
-    .map(NipopowProof::from)
+    header_id: &BlockId,
+) -> js_sys::Promise {
+    let header_id_cloned = header_id.0.clone();
+    #[allow(clippy::clone_on_copy)]
+    let node_cloned = node.0.clone();
+    wasm_bindgen_futures::future_to_promise(async move {
+        let proof = ergo_lib::ergo_rest::api::node::get_nipopow_proof_by_header_id(
+            node_cloned,
+            min_chain_length,
+            suffix_len,
+            header_id_cloned,
+        )
+        .await
+        .map_err(to_js)
+        .map(NipopowProof::from)?;
+        Ok(wasm_bindgen::JsValue::from(proof))
+    })
+}
+
+#[wasm_bindgen]
+/// GET on /blocks/{header_id}/proofFor/{tx_id} to request the merkle proof for a given transaction
+/// that belongs to the given header ID.
+pub fn get_blocks_header_id_proof_for_tx_id(
+    node: &NodeConf,
+    header_id: &BlockId,
+    tx_id: &TxId,
+) -> js_sys::Promise {
+    let header_id_cloned = header_id.0.clone();
+    let tx_id_cloned = tx_id.0.clone();
+    #[allow(clippy::clone_on_copy)]
+    let node_cloned = node.0.clone();
+    wasm_bindgen_futures::future_to_promise(async move {
+        let merkle_proof = ergo_lib::ergo_rest::api::node::get_blocks_header_id_proof_for_tx_id(
+            node_cloned,
+            header_id_cloned,
+            tx_id_cloned,
+        )
+        .await
+        .map_err(to_js)
+        .map(|m| m.map(crate::merkleproof::MerkleProof))?;
+        Ok(wasm_bindgen::JsValue::from(merkle_proof))
+    })
 }
 
 /// List of peer urls returned from `peer_discovery`. We need this wrapper struct because the
diff --git a/bindings/ergo-lib-wasm/src/rest/node_conf.rs b/bindings/ergo-lib-wasm/src/rest/node_conf.rs
index 3c715cd58..f652e5472 100644
--- a/bindings/ergo-lib-wasm/src/rest/node_conf.rs
+++ b/bindings/ergo-lib-wasm/src/rest/node_conf.rs
@@ -12,7 +12,7 @@ use crate::error_conversion::to_js;
 /// Node configuration
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone, Copy, From, Into)]
-pub struct NodeConf(ergo_lib::ergo_rest::NodeConf);
+pub struct NodeConf(pub(crate) ergo_lib::ergo_rest::NodeConf);
 
 #[wasm_bindgen]
 impl NodeConf {
diff --git a/bindings/ergo-lib-wasm/src/rest/node_info.rs b/bindings/ergo-lib-wasm/src/rest/node_info.rs
new file mode 100644
index 000000000..f3950497b
--- /dev/null
+++ b/bindings/ergo-lib-wasm/src/rest/node_info.rs
@@ -0,0 +1,23 @@
+//! Node info
+
+use derive_more::{From, Into};
+use wasm_bindgen::prelude::*;
+
+/// Node info
+#[wasm_bindgen]
+#[derive(Debug, Clone, From, Into)]
+pub struct NodeInfo(pub(crate) ergo_lib::ergo_rest::NodeInfo);
+
+#[wasm_bindgen]
+impl NodeInfo {
+    /// Get name of the ergo node
+    pub fn name(&self) -> String {
+        self.0.name.clone()
+    }
+
+    /// Returns true iff the ergo node is at least v4.0.28. This is important since nipopow proofs
+    /// only work correctly from this version onwards.
+    pub fn is_at_least_version_4_0_28(&self) -> bool {
+        self.0.is_at_least_version_4_0_28()
+    }
+}
diff --git a/bindings/ergo-lib-wasm/src/transaction.rs b/bindings/ergo-lib-wasm/src/transaction.rs
index 20dcc5f15..54e7d88a1 100644
--- a/bindings/ergo-lib-wasm/src/transaction.rs
+++ b/bindings/ergo-lib-wasm/src/transaction.rs
@@ -142,7 +142,7 @@ pub fn extract_hints(
 /// Transaction id
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone, From, Into)]
-pub struct TxId(chain::transaction::TxId);
+pub struct TxId(pub(crate) chain::transaction::TxId);
 
 #[wasm_bindgen]
 impl TxId {
diff --git a/ergo-nipopow/src/nipopow_verifier.rs b/ergo-nipopow/src/nipopow_verifier.rs
index 75c936e6f..57b55a5b6 100644
--- a/ergo-nipopow/src/nipopow_verifier.rs
+++ b/ergo-nipopow/src/nipopow_verifier.rs
@@ -21,6 +21,11 @@ impl NipopowVerifier {
         }
     }
 
+    /// Returns best proof
+    pub fn best_proof(&self) -> Option<NipopowProof> {
+        self.best_proof.clone()
+    }
+
     /// Returns chain of `Header`s from the best proof.
     pub fn best_chain(&self) -> Vec<Header> {
         self.best_proof
@@ -37,6 +42,8 @@ impl NipopowVerifier {
                     if new_proof.is_better_than(p)? {
                         self.best_proof = Some(new_proof);
                     }
+                } else {
+                    self.best_proof = Some(new_proof);
                 }
             }
         }
diff --git a/ergo-rest/Cargo.toml b/ergo-rest/Cargo.toml
index 13859b548..71d06efa4 100644
--- a/ergo-rest/Cargo.toml
+++ b/ergo-rest/Cargo.toml
@@ -21,6 +21,8 @@ sigma-ser = { version = "^0.6.0", path = "../sigma-ser" }
 sigma-util = { version = "^0.5.0", path = "../sigma-util" }
 ergo-chain-types = { version = "^0.4.0", path = "../ergo-chain-types" }
 ergo-nipopow = { version = "^0.4.0", path = "../ergo-nipopow" }
+ergotree-ir = { version = "^0.17.0", path = "../ergotree-ir", features = ["json"] }
+ergo-merkle-tree = { version = "^0.4", path = "../ergo-merkle-tree" }
 futures = "0.3"
 thiserror = "1"
 derive_more = "0.99"
diff --git a/ergo-rest/src/api/node.rs b/ergo-rest/src/api/node.rs
index dda684764..cd4ee6d5a 100644
--- a/ergo-rest/src/api/node.rs
+++ b/ergo-rest/src/api/node.rs
@@ -3,7 +3,10 @@
 use bounded_integer::BoundedU16;
 use bounded_vec::NonEmptyVec;
 use ergo_chain_types::BlockId;
+use ergo_chain_types::Header;
+use ergo_merkle_tree::MerkleProof;
 use ergo_nipopow::NipopowProof;
+use ergotree_ir::chain::tx_id::TxId;
 use std::time::Duration;
 use url::Url;
 
@@ -28,6 +31,23 @@ pub async fn get_info(node: NodeConf) -> Result<NodeInfo, NodeError> {
         .await?)
 }
 
+/// GET on /blocks/{header_id}/header endpoint
+pub async fn get_header(node: NodeConf, header_id: BlockId) -> Result<Header, NodeError> {
+    let header_str = String::from(header_id.0);
+    let mut path = "blocks/".to_owned();
+    path.push_str(&*header_str);
+    path.push_str("/header");
+    #[allow(clippy::unwrap_used)]
+    let url = node.addr.as_http_url().join(&*path).unwrap();
+    let client = build_client(&node)?;
+    let rb = client.get(url);
+    Ok(set_req_headers(rb, node)
+        .send()
+        .await?
+        .json::<Header>()
+        .await?)
+}
+
 /// Given a list of seed nodes, search for peer nodes with an active REST API on port 9053.
 ///  - `seeds` represents a list of ergo node URLs from which to start peer discovery.
 ///  - `max_parallel_requests` represents the maximum number of HTTP requests that can be made in
@@ -72,13 +92,29 @@ pub async fn get_nipopow_proof_by_header_id(
         .await?)
 }
 
-// pub async fn get_blocks_header_id_proof_for_tx_id(
-//     _node: NodeConf,
-//     _header_id: BlockId,
-//     _tx_id: TxId,
-// ) -> Result<Option<MerkleProof>, NodeError> {
-//     todo!()
-// }
+/// GET on /blocks/{header_id}/proofFor/{tx_id} to request the merkle proof for a given transaction
+/// that belongs to the given header ID.
+pub async fn get_blocks_header_id_proof_for_tx_id(
+    node: NodeConf,
+    header_id: BlockId,
+    tx_id: TxId,
+) -> Result<Option<MerkleProof>, NodeError> {
+    let header_str = String::from(header_id.0);
+    let mut path = "blocks/".to_owned();
+    path.push_str(&*header_str);
+    path.push_str("/proofFor/");
+    let tx_id_str = String::from(tx_id);
+    path.push_str(&*tx_id_str);
+    #[allow(clippy::unwrap_used)]
+    let url = node.addr.as_http_url().join(&*path).unwrap();
+    let client = build_client(&node)?;
+    let rb = client.get(url);
+    Ok(set_req_headers(rb, node)
+        .send()
+        .await?
+        .json::<Option<MerkleProof>>()
+        .await?)
+}
 
 #[allow(clippy::unwrap_used)]
 #[cfg(test)]
@@ -124,13 +160,14 @@ mod tests {
             api_key: None,
             timeout: Some(Duration::from_secs(5)),
         };
-        let m = 3;
-        let k = 4;
+        let m = 7;
+        let k = 6;
         let res = runtime_inner.block_on(async {
-            get_nipopow_proof_by_header_id(node_conf, m, k, header_id)
+            get_nipopow_proof_by_header_id(node_conf, m, k, header_id.clone())
                 .await
                 .unwrap()
         });
+        assert_eq!(res.suffix_head.header.id, header_id);
         assert!(!res.prefix.is_empty());
         assert_eq!(res.m, m);
         assert_eq!(res.k, k);
diff --git a/ergo-rest/src/node_info.rs b/ergo-rest/src/node_info.rs
index 17ec60d90..3a5d343e4 100644
--- a/ergo-rest/src/node_info.rs
+++ b/ergo-rest/src/node_info.rs
@@ -1,3 +1,5 @@
+use std::cmp::Ordering;
+
 use serde::{Deserialize, Serialize};
 
 use crate::NodeResponse;
@@ -8,6 +10,18 @@ use crate::NodeResponse;
 pub struct NodeInfo {
     /// Node name
     pub name: String,
+    /// Ergo node app version
+    #[serde(rename = "appVersion")]
+    pub app_version: String,
+}
+
+impl NodeInfo {
+    /// Returns true iff the ergo node is at least v4.0.28. This is important since nipopow proofs
+    /// only work correctly from this version onwards.
+    pub fn is_at_least_version_4_0_28(&self) -> bool {
+        let ord = self.app_version.cmp(&String::from("4.0.28"));
+        ord == Ordering::Equal || ord == Ordering::Greater
+    }
 }
 
 impl NodeResponse for NodeInfo {}
diff --git a/ergo-rest/src/node_response.rs b/ergo-rest/src/node_response.rs
index a851e88c0..46be5849c 100644
--- a/ergo-rest/src/node_response.rs
+++ b/ergo-rest/src/node_response.rs
@@ -1,2 +1,5 @@
 /// Marker trait for response data types
 pub trait NodeResponse {}
+
+impl NodeResponse for ergo_merkle_tree::MerkleProof {}
+impl NodeResponse for ergo_chain_types::Header {}


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js b/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
index e9519f87f..2e5e45508 100644
--- a/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
+++ b/bindings/ergo-lib-wasm/tests_browser/test_rest_api.js
@@ -1,5 +1,4 @@
 import { expect, assert } from "chai";
-import { generate_block_headers } from '../tests/utils';
 
 import * as ergo from "..";
 let ergo_wasm;
@@ -14,13 +13,56 @@ beforeEach(async () => {
 it('node REST API: get_nipopow_proof_by_header_id endpoint', async () => {
     let node_conf = new ergo_wasm.NodeConf("213.239.193.208:9053");
     assert(node_conf != null);
-    const block_headers = generate_block_headers();
-    const header_id = block_headers.get(0).id();
+    const header_id = ergo_wasm.BlockId.from_str("4caa17e62fe66ba7bd69597afdc996ae35b1ff12e0ba90c22ff288a4de10e91b");
     let res = await ergo_wasm.get_nipopow_proof_by_header_id(node_conf, 3, 4, header_id);
     assert(res != null);
     assert(node_conf != null);
 });
 
+it('node REST API: example SPV workflow', async () => {
+    const header_id = ergo_wasm.BlockId.from_str("d1366f762e46b7885496aaab0c42ec2950b0422d48aec3b91f45d4d0cdeb41e5")
+    assert(header_id != null);
+    let tx_id = ergo_wasm.TxId.from_str("258ddfc09b94b8313bca724de44a0d74010cab26de379be845713cc129546b78");
+    assert(tx_id != null);
+
+    // Get NiPoPow proofs from 2 separate ergo nodes
+    let proofs = await Promise.all([
+        get_nipopow_proof("147.135.70.51:9053", header_id),
+        get_nipopow_proof("49.205.198.127:9053", header_id),
+    ]);
+
+    const genesis_block_id = ergo_wasm.BlockId.from_str("b0244dfc267baca974a4caee06120321562784303a8a688976ae56170e4d175b");
+    let verifier = new ergo_wasm.NipopowVerifier(genesis_block_id);
+    assert(verifier != null, "verifier should be non-null");
+    verifier.process(proofs[0]);
+    verifier.process(proofs[1]);
+    let best_proof = verifier.best_proof();
+    assert(best_proof != null, "best proof should exist");
+    assert(best_proof.suffix_head().id().equals(header_id), "equality");
+
+    // Verify with a 3rd node
+    let node_conf = new ergo_wasm.NodeConf("15.235.145.2:9053");
+    let header = await ergo_wasm.get_header(node_conf, header_id);
+    assert(header != null, "header should be non-null");
+    let merkle_proof = await ergo_wasm.get_blocks_header_id_proof_for_tx_id(node_conf, header_id, tx_id);
+    assert(merkle_proof != null);
+    assert(merkle_proof.valid(header.transactions_root()));
+});
+
+async function get_nipopow_proof(node_addr, header_id) {
+    let node_conf = new ergo_wasm.NodeConf(node_addr);
+    assert(node_conf != null);
+
+    // Make sure we're communicating with a node with version >= 4.0.28, otherwise we won't be able
+    // to get a proper Nipopow proof
+    let node_info = await ergo_wasm.get_info(node_conf);
+    assert(node_info.is_at_least_version_4_0_28());
+
+    let proof = await ergo_wasm.get_nipopow_proof_by_header_id(node_conf, 7, 6, header_id);
+    assert(proof != null);
+    return proof;
+}
+
 it('node REST API: peer_discovery endpoint', async () => {
     const seeds = [
         "http://213.239.193.208:9030",