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

Issue:
Missing serialization for Ledger support
- TokenId::as_bytes()
- Constant::sigma_serialize_bytes()
- BoxId::as_bytes()
- rename ErgoTree::to_bytes() to ErgoTree::sigma_serialize_bytes()
- BoxValue and TokenAmounts to big-endian bytes
- ErgoBox:: serialized_registers (count and serialized registers)
- UnsignedTransaction::get_distinct_token_ids() return [TokenId]
- ContextExtension::sigma_serialize_bytes
- make Transaction from UnsignedTransaction + [ProofBytes]


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-wasm/src/ast.rs b/bindings/ergo-lib-wasm/src/ast.rs
index 7ead05cc3..2767ddc63 100644
--- a/bindings/ergo-lib-wasm/src/ast.rs
+++ b/bindings/ergo-lib-wasm/src/ast.rs
@@ -40,6 +40,13 @@ impl Constant {
         self.0.base16_str()
     }
 
+    /// Returns serialized bytes or fails with error if Constant cannot be serialized
+    pub fn sigma_serialize_bytes(&self) -> Result<Vec<u8>, JsValue> {
+        self.0
+            .sigma_serialize_bytes()
+            .map_err(|e| JsValue::from_str(&format! {"{:?}", e}))
+    }
+
     /// Create from i32 value
     pub fn from_i32(v: i32) -> Constant {
         Constant(v.into())
diff --git a/bindings/ergo-lib-wasm/src/context_extension.rs b/bindings/ergo-lib-wasm/src/context_extension.rs
index b96004c16..d5b9f04d7 100644
--- a/bindings/ergo-lib-wasm/src/context_extension.rs
+++ b/bindings/ergo-lib-wasm/src/context_extension.rs
@@ -1,6 +1,7 @@
 //! ProverResult
 
 use crate::ast::Constant;
+use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
 use wasm_bindgen::prelude::*;
 
 extern crate derive_more;
@@ -34,4 +35,11 @@ impl ContextExtension {
             self.0.clone();
         wrapped.values.keys().cloned().collect()
     }
+
+    /// Returns serialized bytes or fails with error if ContextExtension cannot be serialized
+    pub fn sigma_serialize_bytes(&self) -> Result<Vec<u8>, JsValue> {
+        self.0
+            .sigma_serialize_bytes()
+            .map_err(|e| JsValue::from_str(&format! {"{:?}", e}))
+    }
 }
diff --git a/bindings/ergo-lib-wasm/src/ergo_box.rs b/bindings/ergo-lib-wasm/src/ergo_box.rs
index ea12c6a76..0aaa7b492 100644
--- a/bindings/ergo-lib-wasm/src/ergo_box.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_box.rs
@@ -20,6 +20,8 @@ use std::convert::TryFrom;
 
 use chain::ergo_box::NonMandatoryRegisters;
 use ergo_lib::chain;
+use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
+use js_sys::Uint8Array;
 use wasm_bindgen::prelude::*;
 
 use crate::ast::Constant;
@@ -53,6 +55,11 @@ impl BoxId {
     pub fn to_str(&self) -> String {
         self.0.clone().into()
     }
+
+    /// Returns byte array (32 bytes)
+    pub fn as_bytes(&self) -> Uint8Array {
+        Uint8Array::from(self.0.as_ref())
+    }
 }
 
 /// ErgoBox candidate not yet included in any transaction on the chain
@@ -173,6 +180,15 @@ impl ErgoBox {
     pub fn from_json(json: &str) -> Result<ErgoBox, JsValue> {
         serde_json::from_str(json).map(Self).map_err(to_js)
     }
+
+    /// Serialized additional register as defined in ErgoBox serialization (registers count,
+    /// followed by every non-empyt register value serialized)
+    pub fn serialized_additional_registers(&self) -> Result<Vec<u8>, JsValue> {
+        self.0
+            .additional_registers
+            .sigma_serialize_bytes()
+            .map_err(|e| JsValue::from_str(&format!("{}", e)))
+    }
 }
 
 impl From<ErgoBox> for chain::ergo_box::ErgoBox {
@@ -218,6 +234,11 @@ impl BoxValue {
     pub fn as_i64(&self) -> I64 {
         self.0.as_i64().into()
     }
+
+    /// big-endian byte array representation
+    pub fn to_bytes(&self) -> Vec<u8> {
+        self.0.as_u64().to_be_bytes().to_vec()
+    }
 }
 
 impl From<BoxValue> for chain::ergo_box::BoxValue {
diff --git a/bindings/ergo-lib-wasm/src/ergo_tree.rs b/bindings/ergo-lib-wasm/src/ergo_tree.rs
index e5e3194b1..dce51cf75 100644
--- a/bindings/ergo-lib-wasm/src/ergo_tree.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_tree.rs
@@ -30,8 +30,8 @@ impl ErgoTree {
             .map(ErgoTree)
             .map_err(to_js)
     }
-    /// Encode Ergo tree as serialized bytes
-    pub fn to_bytes(&self) -> Result<Vec<u8>, JsValue> {
+    /// Returns serialized bytes or fails with error if ErgoTree cannot be serialized
+    pub fn sigma_serialize_bytes(&self) -> Result<Vec<u8>, JsValue> {
         self.0.sigma_serialize_bytes().map_err(to_js)
     }
 
diff --git a/bindings/ergo-lib-wasm/src/token.rs b/bindings/ergo-lib-wasm/src/token.rs
index 43ead1334..3cdec7769 100644
--- a/bindings/ergo-lib-wasm/src/token.rs
+++ b/bindings/ergo-lib-wasm/src/token.rs
@@ -5,6 +5,7 @@ use std::convert::TryFrom;
 use ergo_lib::chain;
 use ergo_lib::chain::Base16DecodedBytes;
 use ergo_lib::chain::Digest32;
+use js_sys::Uint8Array;
 use wasm_bindgen::prelude::*;
 
 use crate::ergo_box::BoxId;
@@ -38,6 +39,11 @@ impl TokenId {
     pub fn to_str(&self) -> String {
         self.0.clone().into()
     }
+
+    /// Returns byte array (32 bytes)
+    pub fn as_bytes(&self) -> Uint8Array {
+        Uint8Array::from(self.0.as_ref())
+    }
 }
 
 impl From<TokenId> for chain::token::TokenId {
@@ -64,6 +70,11 @@ impl TokenAmount {
     pub fn as_i64(&self) -> I64 {
         i64::from(self.0).into()
     }
+
+    /// big-endian byte array representation
+    pub fn to_bytes(&self) -> Vec<u8> {
+        self.0.as_u64().to_be_bytes().to_vec()
+    }
 }
 
 impl From<TokenAmount> for chain::token::TokenAmount {
diff --git a/bindings/ergo-lib-wasm/src/transaction.rs b/bindings/ergo-lib-wasm/src/transaction.rs
index c26468e6c..b103a97ea 100644
--- a/bindings/ergo-lib-wasm/src/transaction.rs
+++ b/bindings/ergo-lib-wasm/src/transaction.rs
@@ -6,6 +6,8 @@ use crate::data_input::DataInputs;
 use crate::error_conversion::to_js;
 use crate::input::{Inputs, UnsignedInputs};
 use ergo_lib::chain;
+use ergo_lib::chain::transaction::distinct_token_ids;
+use js_sys::Uint8Array;
 use std::convert::TryFrom;
 use std::convert::TryInto;
 use wasm_bindgen::prelude::*;
@@ -64,6 +66,23 @@ pub struct Transaction(chain::transaction::Transaction);
 
 #[wasm_bindgen]
 impl Transaction {
+    /// Create Transaction from UnsignedTransaction and an array of proofs in the same order as
+    /// UnsignedTransaction.inputs with empty proof indicated with empty byte array
+    pub fn from_unsigned_tx(
+        unsigned_tx: UnsignedTransaction,
+        proofs: Vec<Uint8Array>,
+    ) -> Result<Transaction, JsValue> {
+        chain::transaction::Transaction::from_unsigned_tx(
+            unsigned_tx.0,
+            proofs
+                .into_iter()
+                .map(|bytes| bytes.to_vec().into())
+                .collect(),
+        )
+        .map_err(|e| JsValue::from_str(&format!("{}", e)))
+        .map(Into::into)
+    }
+
     /// Get id for transaction
     pub fn id(&self) -> TxId {
         self.0.id().into()
@@ -152,6 +171,14 @@ impl UnsignedTransaction {
     pub fn from_json(json: &str) -> Result<UnsignedTransaction, JsValue> {
         serde_json::from_str(json).map(Self).map_err(to_js)
     }
+
+    /// Returns distinct token id from output_candidates as array of byte arrays
+    pub fn distinct_token_ids(&self) -> Vec<Uint8Array> {
+        distinct_token_ids(self.0.output_candidates.clone())
+            .iter()
+            .map(|id| Uint8Array::from(id.as_ref()))
+            .collect()
+    }
 }
 
 impl From<chain::transaction::unsigned::UnsignedTransaction> for UnsignedTransaction {
diff --git a/ergo-lib/CHANGELOG.md b/ergo-lib/CHANGELOG.md
index dbedd0d0c..de24fa2bc 100644
--- a/ergo-lib/CHANGELOG.md
+++ b/ergo-lib/CHANGELOG.md
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Added 
 - `Box.bytes` [#390](https://github.com/ergoplatform/sigma-rust/pull/390);
 - add (Coll[Byte], Coll[Byte]) and (Long, Long) support for Constant conversion in JS [#386](https://github.com/ergoplatform/sigma-rust/pull/386);
+- add `ErgoBox::serialized_additional_registers()` in Wasm [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
+- add `Transaction::from_unsigned_tx()` to construct from unsigned tx + proofs [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
+- extract distinct token ids on tx serialization and expose as `UnsignedTransaction::distinct_token_ids()` in Wasm [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
+- add `to_bytes()` for `BoxValue` and `TokenAmount` in Wasm [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
+- `Constant::sigma_serialize_bytes()`, `ContextExtension::sigma_serialize_bytes()` in Wasm [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
+- `TokenId::as_bytes()` and `BoxId::as_bytes()` in Wasm [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
 - `Coll.slice` [#309](https://github.com/ergoplatform/sigma-rust/pull/309);
 - Byte-wise XOR for byte arrays [#310](https://github.com/ergoplatform/sigma-rust/pull/310);
 - `Constant::from_i64_str_array` and `to_i64_str_array` for `Coll[Long]` encoding [#311](https://github.com/ergoplatform/sigma-rust/pull/311);
@@ -19,6 +25,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 - Better Debug print for EC point [#319](https://github.com/ergoplatform/sigma-rust/pull/319);
 - `Constant::from_ecpoint_bytes` to Wasm API [#324](https://github.com/ergoplatform/sigma-rust/pull/324);
 
+
+### Changed(BREAKING!):
+- `ErgoTree::to_bytes()` renamed to  `ErgoTree::sigma_serialize_bytes()` in Wasm [#387](https://github.com/ergoplatform/sigma-rust/pull/387);
+
 ### Changed
 - `SigmaSerializable:sigma_serialize` errors are extended beyond `io::Error` [#328](https://github.com/ergoplatform/sigma-rust/pull/328);
 - `SigmaSerializable:sigma_serialize_bytes` made failible (returns `Result`) [#328](https://github.com/ergoplatform/sigma-rust/pull/328);
diff --git a/ergo-lib/src/chain/digest32.rs b/ergo-lib/src/chain/digest32.rs
index 06df5bc0e..43079e8a3 100644
--- a/ergo-lib/src/chain/digest32.rs
+++ b/ergo-lib/src/chain/digest32.rs
@@ -115,6 +115,12 @@ impl<const N: usize> SigmaSerializable for Digest<N> {
     }
 }
 
+impl AsRef<[u8]> for Digest32 {
+    fn as_ref(&self) -> &[u8] {
+        &self.0[..]
+    }
+}
+
 /// Invalid byte array size
 #[derive(Error, Debug)]
 pub enum Digest32Error {
diff --git a/ergo-lib/src/chain/ergo_box.rs b/ergo-lib/src/chain/ergo_box.rs
index bb40b2930..95b6437cc 100644
--- a/ergo-lib/src/chain/ergo_box.rs
+++ b/ergo-lib/src/chain/ergo_box.rs
@@ -482,16 +482,7 @@ pub fn serialize_box_with_indexed_digests<W: SigmaByteWrite>(
         }
         .and_then(|()| Ok(w.put_u64(t.amount.into())?))
     })?;
-
-    let regs_num = additional_registers.len();
-    w.put_u8(regs_num as u8)?;
-
-    additional_registers
-        .get_ordered_values()
-        .iter()
-        .try_for_each(|c| c.sigma_serialize(w))?;
-
-    Ok(())
+    additional_registers.sigma_serialize(w)
 }
 
 /// Box deserialization with token ids optionally parsed in transaction
@@ -526,13 +517,8 @@ pub fn parse_box_with_indexed_digests<R: SigmaByteRead>(
         })
     }
 
-    let regs_num = r.get_u8()?;
-    let mut additional_regs = Vec::with_capacity(regs_num as usize);
-    for _ in 0..regs_num {
-        let v = Constant::sigma_parse(r)?;
-        additional_regs.push(v);
-    }
-    let additional_registers = NonMandatoryRegisters::from_ordered_values(additional_regs)?;
+    let additional_registers = NonMandatoryRegisters::sigma_parse(r)?;
+
     Ok(ErgoBoxCandidate {
         value,
         ergo_tree,
diff --git a/ergo-lib/src/chain/ergo_box/box_id.rs b/ergo-lib/src/chain/ergo_box/box_id.rs
index adabb8816..e761ffa01 100644
--- a/ergo-lib/src/chain/ergo_box/box_id.rs
+++ b/ergo-lib/src/chain/ergo_box/box_id.rs
@@ -36,6 +36,12 @@ impl BoxId {
     }
 }
 
+impl AsRef<[u8]> for BoxId {
+    fn as_ref(&self) -> &[u8] {
+        self.0.as_ref()
+    }
+}
+
 #[cfg(feature = "json")]
 impl From<BoxId> for String {
     fn from(v: BoxId) -> Self {
diff --git a/ergo-lib/src/chain/ergo_box/register.rs b/ergo-lib/src/chain/ergo_box/register.rs
index 5ea5077e5..4f5e65ebc 100644
--- a/ergo-lib/src/chain/ergo_box/register.rs
+++ b/ergo-lib/src/chain/ergo_box/register.rs
@@ -4,7 +4,11 @@
 use crate::chain::json::ergo_box::ConstantHolder;
 use crate::chain::Base16EncodedBytes;
 use ergotree_ir::mir::constant::Constant;
+use ergotree_ir::serialization::sigma_byte_reader::SigmaByteRead;
+use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite;
 use ergotree_ir::serialization::SigmaParsingError;
+use ergotree_ir::serialization::SigmaSerializable;
+use ergotree_ir::serialization::SigmaSerializeResult;
 #[cfg(feature = "json")]
 use serde::{Deserialize, Serialize};
 use std::convert::TryInto;
@@ -212,6 +216,27 @@ impl NonMandatoryRegisters {
     }
 }
 
+impl SigmaSerializable for NonMandatoryRegisters {
+    fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
+        let regs_num = self.len();
+        w.put_u8(regs_num as u8)?;
+
+        self.get_ordered_values()
+            .iter()
+            .try_for_each(|c| c.sigma_serialize(w))
+    }
+
+    fn sigma_parse<R: SigmaByteRead>(r: &mut R) -> Result<Self, SigmaParsingError> {
+        let regs_num = r.get_u8()?;
+        let mut additional_regs = Vec::with_capacity(regs_num as usize);
+        for _ in 0..regs_num {
+            let v = Constant::sigma_parse(r)?;
+            additional_regs.push(v);
+        }
+        Ok(NonMandatoryRegisters::from_ordered_values(additional_regs)?)
+    }
+}
+
 /// Possible errors when building NonMandatoryRegisters
 #[derive(Error, PartialEq, Eq, Clone, Debug)]
 pub enum NonMandatoryRegistersError {
@@ -309,6 +334,7 @@ impl TryFrom<i8> for MandatoryRegisterId {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use ergotree_ir::serialization::sigma_serialize_roundtrip;
     use proptest::{arbitrary::Arbitrary, collection::vec, prelude::*};
 
     impl Arbitrary for NonMandatoryRegisters {
@@ -348,6 +374,11 @@ mod tests {
         fn reg_id_from_byte(reg_id_byte in 0i8..NonMandatoryRegisterId::END_INDEX as i8) {
             assert!(RegisterId::try_from(reg_id_byte).is_ok());
         }
+
+        #[test]
+        fn ser_roundtrip(regs in any::<NonMandatoryRegisters>()) {
+            prop_assert_eq![sigma_serialize_roundtrip(&regs), regs];
+        }
     }
 
     #[test]
diff --git a/ergo-lib/src/chain/token.rs b/ergo-lib/src/chain/token.rs
index 854f60ae6..13d16b95d 100644
--- a/ergo-lib/src/chain/token.rs
+++ b/ergo-lib/src/chain/token.rs
@@ -39,6 +39,18 @@ impl From<TokenId> for Vec<i8> {
     }
 }
 
+impl From<TokenId> for Vec<u8> {
+    fn from(v: TokenId) -> Self {
+        v.0.into()
+    }
+}
+
+impl AsRef<[u8]> for TokenId {
+    fn as_ref(&self) -> &[u8] {
+        self.0.as_ref()
+    }
+}
+
 impl From<TokenId> for String {
     fn from(v: TokenId) -> Self {
         v.0.into()
@@ -90,6 +102,11 @@ impl TokenAmount {
             Ok(Self(raw))
         }
     }
+
+    /// Get the value as u64
+    pub fn as_u64(&self) -> &u64 {
+        &self.0
+    }
 }
 
 /// BoxValue errors
diff --git a/ergo-lib/src/chain/transaction.rs b/ergo-lib/src/chain/transaction.rs
index 734fd4dd9..aada04559 100644
--- a/ergo-lib/src/chain/transaction.rs
+++ b/ergo-lib/src/chain/transaction.rs
@@ -5,7 +5,10 @@ pub mod input;
 pub mod unsigned;
 
 use bounded_vec::BoundedVec;
+use thiserror::Error;
+
 pub use data_input::*;
+use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
 use ergotree_ir::serialization::sigma_byte_reader::SigmaByteRead;
 use ergotree_ir::serialization::sigma_byte_writer::SigmaByteWrite;
 use ergotree_ir::serialization::SigmaParsingError;
@@ -14,6 +17,8 @@ use ergotree_ir::serialization::SigmaSerializationError;
 use ergotree_ir::serialization::SigmaSerializeResult;
 pub use input::*;
 
+use self::unsigned::UnsignedTransaction;
+
 #[cfg(feature = "json")]
 use super::json;
 use super::{
@@ -31,8 +36,6 @@ use serde::{Deserialize, Serialize};
 use std::convert::TryFrom;
 use std::convert::TryInto;
 use std::iter::FromIterator;
-#[cfg(feature = "json")]
-use thiserror::Error;
 
 /// Transaction id (ModifierId in sigmastate)
 #[derive(PartialEq, Eq, Hash, Debug, Clone)]
@@ -64,6 +67,12 @@ impl From<TxId> for String {
     }
 }
 
+impl AsRef<[u8]> for TxId {
+    fn as_ref(&self) -> &[u8] {
+        self.0.as_ref()
+    }
+}
+
 /// BouncedVec type for Tx inputs and output_candidates
 pub type TxIoVec<T> = BoundedVec<T, 1, { u16::MAX as usize }>;
 
@@ -134,6 +143,33 @@ impl Transaction {
         })
     }
 
+    /// Create Transaction from UnsignedTransaction and an array of proofs in the same order as
+    /// UnsignedTransaction.inputs
+    pub fn from_unsigned_tx(
+        unsigned_tx: UnsignedTransaction,
+        proofs: Vec<ProofBytes>,
+    ) -> Result<Self, TransactionError> {
+        let inputs = unsigned_tx
+            .inputs
+            .enumerated()
+            .try_mapped(|(index, unsigned_input)| {
+                proofs
+                    .get(index)
+                    .map(|proof| Input::from_unsigned_input(unsigned_input, proof.clone()))
+                    .ok_or_else(|| {
+                        TransactionError::InvalidArgument(format!(
+                            "no proof for input index: {}",
+                            index
+                        ))
+                    })
+            })?;
+        Ok(Transaction::new(
+            inputs,
+            unsigned_tx.data_inputs,
+            unsigned_tx.output_candidates,
+        )?)
+    }
+
     fn calc_tx_id(&self) -> Result<TxId, SigmaSerializationError> {
         let bytes = self.bytes_to_sign()?;
         Ok(TxId(blake2b256_hash(&bytes)))
@@ -155,6 +191,23 @@ impl Transaction {
     }
 }
 
+/// Returns distinct token ids from all given ErgoBoxCandidate's
+pub fn distinct_token_ids<I>(output_candidates: I) -> IndexSet<TokenId>
+where
+    I: IntoIterator<Item = ErgoBoxCandidate>,
+{
+    let token_ids: Vec<TokenId> = output_candidates
+        .into_iter()
+        .flat_map(|b| {
+            b.tokens
+                .iter()
+                .map(|t| t.token_id.clone())
+                .collect::<Vec<TokenId>>()
+        })
+        .collect();
+    IndexSet::<_>::from_iter(token_ids)
+}
+
 impl SigmaSerializable for Transaction {
     fn sigma_serialize<W: SigmaByteWrite>(&self, w: &mut W) -> SigmaSerializeResult {
         // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala#L112-L112
@@ -170,12 +223,7 @@ impl SigmaSerializable for Transaction {
         // Serialize distinct ids of tokens in transaction outputs.
         // This optimization is crucial to allow up to MaxTokens (== 255) in a box.
         // Without it total size of all token ids 255 * 32 = 8160, way beyond MaxBoxSize (== 4K)
-        let token_ids: Vec<TokenId> = self
-            .output_candidates
-            .iter()
-            .flat_map(|b| b.tokens.iter().map(|t| t.token_id.clone()))
-            .collect();
-        let distinct_token_ids: IndexSet<TokenId> = IndexSet::from_iter(token_ids);
+        let distinct_token_ids = distinct_token_ids(self.output_candidates.clone());
         w.put_u32(u32::try_from(distinct_token_ids.len()).unwrap())?;
         distinct_token_ids
             .iter()
@@ -236,6 +284,17 @@ impl SigmaSerializable for Transaction {
     }
 }
 
+/// Error when working with Transaction
+#[derive(Error, Eq, PartialEq, Debug, Clone)]
+pub enum TransactionError {
+    /// Serialization error
+    #[error("Tx serialization error: {0}")]
+    SigmaSerializationError(#[from] SigmaSerializationError),
+    /// Invalid argument on tx construction
+    #[error("Tx innvalid argument: {0}")]
+    InvalidArgument(String),
+}
+
 #[cfg(feature = "json")]
 impl From<Transaction> for json::transaction::TransactionJson {
     fn from(v: Transaction) -> Self {
diff --git a/ergo-lib/src/chain/transaction/input.rs b/ergo-lib/src/chain/transaction/input.rs
index 1b6e7d4a5..cde1910f3 100644
--- a/ergo-lib/src/chain/transaction/input.rs
+++ b/ergo-lib/src/chain/transaction/input.rs
@@ -88,6 +88,17 @@ impl Input {
         }
     }
 
+    /// Create Input from UnsignedInput and a proof
+    pub fn from_unsigned_input(unsigned_input: UnsignedInput, proof_bytes: ProofBytes) -> Self {
+        Self::new(
+            unsigned_input.box_id,
+            ProverResult {
+                proof: proof_bytes,
+                extension: unsigned_input.extension,
+            },
+        )
+    }
+
     /// input with an empty proof
     pub fn input_to_sign(&self) -> Input {
         Input {
diff --git a/ergo-lib/src/chain/transaction/unsigned.rs b/ergo-lib/src/chain/transaction/unsigned.rs
index eef8d9bf2..be85dd383 100644
--- a/ergo-lib/src/chain/transaction/unsigned.rs
+++ b/ergo-lib/src/chain/transaction/unsigned.rs
@@ -1,5 +1,8 @@
 //! Unsigned (without proofs) transaction
 
+use crate::chain::token::TokenId;
+
+use super::distinct_token_ids;
 use super::input::{Input, UnsignedInput};
 #[cfg(feature = "json")]
 use super::json;
@@ -12,6 +15,7 @@ use super::{
 };
 use ergotree_interpreter::sigma_protocol::prover::ProofBytes;
 use ergotree_ir::serialization::SigmaSerializationError;
+use indexmap::IndexSet;
 #[cfg(feature = "json")]
 use serde::{Deserialize, Serialize};
 #[cfg(feature = "json")]
@@ -66,13 +70,7 @@ impl UnsignedTransaction {
         Ok(TxId(blake2b256_hash(&bytes)))
     }
 
-    /// Get transaction id
-    pub fn id(&self) -> TxId {
-        self.tx_id.clone()
-    }
-
-    /// message to be signed by the [`ergotree_interpreter::sigma_protocol::prover::Prover`] (serialized tx)
-    pub fn bytes_to_sign(&self) -> Result<Vec<u8>, SigmaSerializationError> {
+    fn to_tx_wo_proofs(&self) -> Result<Transaction, SigmaSerializationError> {
         let empty_proofs_input = self.inputs.mapped_ref(|ui| {
             Input::new(
                 ui.box_id.clone(),
@@ -82,13 +80,28 @@ impl UnsignedTransaction {
                 },
             )
         });
-        let tx = Transaction::new(
+        Transaction::new(
             empty_proofs_input,
             self.data_inputs.clone(),
             self.output_candidates.clone(),
-        )?;
+        )
+    }
+
+    /// Get transaction id
+    pub fn id(&self) -> TxId {
+        self.tx_id.clone()
+    }
+
+    /// message to be signed by the [`ergotree_interpreter::sigma_protocol::prover::Prover`] (serialized tx)
+    pub fn bytes_to_sign(&self) -> Result<Vec<u8>, SigmaSerializationError> {
+        let tx = self.to_tx_wo_proofs()?;
         tx.bytes_to_sign()
     }
+
+    /// Returns distinct token ids from all output_candidates
+    pub fn distinct_token_ids(&self) -> IndexSet<TokenId> {
+        distinct_token_ids(self.output_candidates.clone())
+    }
 }
 
 #[cfg(feature = "json")]


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/test_transaction.js b/bindings/ergo-lib-wasm/tests/test_transaction.js
index fcf552fc1..35b8d8b1f 100644
--- a/bindings/ergo-lib-wasm/tests/test_transaction.js
+++ b/bindings/ergo-lib-wasm/tests/test_transaction.js
@@ -4,32 +4,32 @@ import {
   Address, Wallet, ErgoBox, ErgoBoxCandidateBuilder, Contract,
   ErgoBoxes, ErgoBoxCandidates,
   ErgoStateContext, TxBuilder, BoxValue, BoxSelector, I64, SecretKey, SecretKeys, TxId, DataInputs,
-  SimpleBoxSelector, Tokens, Token, TokenAmount, TokenId, BlockHeaders, PreHeader,
+  SimpleBoxSelector, Tokens, Token, TokenAmount, TokenId, BlockHeaders, PreHeader, Transaction,
 } from '../pkg/ergo_lib_wasm';
 
 const block_headers = BlockHeaders.from_json([{
-      "extensionId": "d16f25b14457186df4c5f6355579cc769261ce1aebc8209949ca6feadbac5a3f",
-      "difficulty": "626412390187008",
-      "votes": "040000",
-      "timestamp": 1618929697400,
-      "size": 221,
-      "stateRoot": "8ad868627ea4f7de6e2a2fe3f98fafe57f914e0f2ef3331c006def36c697f92713",
-      "height": 471746,
-      "nBits": 117586360,
-      "version": 2,
-      "id": "4caa17e62fe66ba7bd69597afdc996ae35b1ff12e0ba90c22ff288a4de10e91b",
-      "adProofsRoot": "d882aaf42e0a95eb95fcce5c3705adf758e591532f733efe790ac3c404730c39",
-      "transactionsRoot": "63eaa9aff76a1de3d71c81e4b2d92e8d97ae572a8e9ab9e66599ed0912dd2f8b",
-      "extensionHash": "3f91f3c680beb26615fdec251aee3f81aaf5a02740806c167c0f3c929471df44",
-      "powSolutions": {
-        "pk": "02b3a06d6eaa8671431ba1db4dd427a77f75a5c2acbd71bfb725d38adc2b55f669",
-        "w": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
-        "n": "5939ecfee6b0d7f4",
-        "d": 0
-      },
-      "adProofsId": "86eaa41f328bee598e33e52c9e515952ad3b7874102f762847f17318a776a7ae",
-      "transactionsId": "ac80245714f25aa2fafe5494ad02a26d46e7955b8f5709f3659f1b9440797b3e",
-      "parentId": "6481752bace5fa5acba5d5ef7124d48826664742d46c974c98a2d60ace229a34"
+  "extensionId": "d16f25b14457186df4c5f6355579cc769261ce1aebc8209949ca6feadbac5a3f",
+  "difficulty": "626412390187008",
+  "votes": "040000",
+  "timestamp": 1618929697400,
+  "size": 221,
+  "stateRoot": "8ad868627ea4f7de6e2a2fe3f98fafe57f914e0f2ef3331c006def36c697f92713",
+  "height": 471746,
+  "nBits": 117586360,
+  "version": 2,
+  "id": "4caa17e62fe66ba7bd69597afdc996ae35b1ff12e0ba90c22ff288a4de10e91b",
+  "adProofsRoot": "d882aaf42e0a95eb95fcce5c3705adf758e591532f733efe790ac3c404730c39",
+  "transactionsRoot": "63eaa9aff76a1de3d71c81e4b2d92e8d97ae572a8e9ab9e66599ed0912dd2f8b",
+  "extensionHash": "3f91f3c680beb26615fdec251aee3f81aaf5a02740806c167c0f3c929471df44",
+  "powSolutions": {
+    "pk": "02b3a06d6eaa8671431ba1db4dd427a77f75a5c2acbd71bfb725d38adc2b55f669",
+    "w": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+    "n": "5939ecfee6b0d7f4",
+    "d": 0
+  },
+  "adProofsId": "86eaa41f328bee598e33e52c9e515952ad3b7874102f762847f17318a776a7ae",
+  "transactionsId": "ac80245714f25aa2fafe5494ad02a26d46e7955b8f5709f3659f1b9440797b3e",
+  "parentId": "6481752bace5fa5acba5d5ef7124d48826664742d46c974c98a2d60ace229a34"
 }]);
 
 it('TxBuilder test', async () => {
@@ -209,3 +209,38 @@ it('use signed tx outputs as inputs in a new tx', async () => {
   const new_tx = tx_builder.build();
   assert(new_tx != null);
 });
+
+it('Transaction::from_unsigned_tx test', async () => {
+  const recipient = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
+  const unspent_boxes = ErgoBoxes.from_boxes_json([
+    {
+      "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+      "value": 67500000000,
+      "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+      "assets": [],
+      "creationHeight": 284761,
+      "additionalRegisters": {},
+      "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+      "index": 1
+    }
+  ]);
+  const contract = Contract.pay_to_address(recipient);
+  const outbox_value = BoxValue.SAFE_USER_MIN();
+  const outbox = new ErgoBoxCandidateBuilder(outbox_value, contract, 0).build();
+  const tx_outputs = new ErgoBoxCandidates(outbox);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
+  const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
+  const min_change_value = BoxValue.SAFE_USER_MIN();
+  const data_inputs = new DataInputs();
+  const box_selector = new SimpleBoxSelector();
+  const target_balance = BoxValue.from_i64(outbox_value.as_i64().checked_add(fee.as_i64()));
+  const box_selection = box_selector.select(unspent_boxes, target_balance, new Tokens());
+  const tx_builder = TxBuilder.new(box_selection, tx_outputs, 0, fee, change_address, min_change_value);
+  tx_builder.set_data_inputs(data_inputs);
+  const tx = tx_builder.build();
+  assert(tx != null);
+  const proof = new Uint8Array([1, 1, 2, 255]);
+  const signed_tx = Transaction.from_unsigned_tx(tx, [proof]);
+  assert(signed_tx != null);
+  assert(signed_tx.inputs().get(0).spending_proof().proof().toString() == proof.toString());
+});