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

Issue:
[5000 SigmaUSD] C + Swift(iOS) bindings 
## Scope
Implement an API feature-wise on par with Wasm bindings (ergo-lib-wasm). Basically an API that a wallet would need to be able to send and receive coins and tokens including input box selection, output box creation and transaction building and signing.

## Architecture
The core part is the common/shared source code with JNI (ergo-lib-jni) that should be implemented in ergo-lib-c-core crate.
The generic C bindings should be implemented in ergo-lib-c which should be used in Swift wrapper that should be implemented in ergo-lib-ios.

### C
The main idea is to pass Rust types wrapped in opaque pointers and expose functions to manipulate them on C side.
The crucial part is to free resources on the same side(usually Rust) where they were allocated. For this we need to expose a separate function for every "resource-producing" function that frees a resource passed from C. In case of Address see address_from_testnet and address_delete.

Use the already implemented Address type as an example. 

### Swift
Wraps functions from ergo-lib-c in classes for resource management and exposes methods converting to/from Swift types where necessary.

## Types
Using Wasm bindings as a reference we need the following types(with methods) implemented. For implmentation details see Wasm types and their methods in ergo-lib-wasm. Some method names are adopted for C types. See implemented Address type

Wallet:
from_secrets
sign_transaction

SecretKey:
dlog_from_bytes
to_bytes

ErgoStateContext:
new(from PreHeader)

PreHeader:
from_block_header

BlockHeader:
from_json

BlockHeaders:
from_json

UnsignedTransaction:
id
inputs
data_inputs
output_candidates
to_json
from_json

ErgoBox:
new
box_id
creation_height
tokens
ergo_tree
value
register_value
to_json
from_json

ErgoBoxCandidate:
creation_height
tokens
ergo_tree
value
register_value

Token:
new
id
amount

TokenId:
from_box_id
from_str
to_str

TokenAmount:
from_int64_t
as_int64_t

BoxValue:
from_int64_t
as_int64_t

Transaction:
id
to_json
from_json
inputs
data_inputs
output_candidates
outputs

DataInput:
new
box_id

TxBuilder:
new
set_data_inputs
build
box_selection
data_inputs
output_candidates
current_height
fee_amount
change_address
min_change_value

ErgoBoxCandidateBuilder:
new
set_min_box_value_per_byte
min_box_value_per_byte
set_value
value
calc_box_size_bytes
calc_min_box_value
set_register_value
register_value
delete_register_value
mint_token
add_token
build

BoxSelection:
new
boxes
change

SimpleBoxSelector:
select

ErgoBoxAssetsData:
new
value
tokens

## Collections
The following types represent a simple wrapper for Vec<T>. Feel free to explore the idea to use the single type (pointer+size) since Rust arrays and slices have the same representation in C (arrays).

ErgoBoxCandidates; 
Tokens;
DataInputs;
ErgoBoxAssetsDataList;
SecretKeys;
ErgoBoxes (instead of UnspentBoxes, OutputBoxes, DataInputBoxes)

All of them should implement the following methods:
new
len
get
add

The ErgoBoxes type should implement from_boxes_json method (see Wasm).



Discussion / hints:
Going to start this one.
> Going to start this one.

Great! Two weeks ago @SethDusek  approached me in DM and expressed interest in this issue. @SethDusek Did you start/plan to work on it?
To clarify more on this task. The thing with C bindings is that their main purpose is to implement Swift/iOS bindings. C bindings should be implemented alongside the Swift bindings so that Swift serves as feedback for C bindings. I'd prefer implementing type by type in both C and Swift simultaneously.
I want to avoid the situation where we need to significantly rewrite C bindings when we start implementing Swift.
Unfortunately, I won't be able to work on it for now, so it's alright with me if someone else takes it
> To clarify more on this task. The thing with C bindings is that their main purpose is to implement Swift/iOS bindings. C bindings should be implemented alongside the Swift bindings so that Swift serves as feedback for C bindings. I'd prefer implementing type by type in both C and Swift simultaneously. I want to avoid the situation where we need to significantly rewrite C bindings when we start implementing Swift.

Thanks for the suggestions and I'll take that approach. I was poking around for C libraries for prop-testing and it's good to know I can lift it to the Swift level.

Fix patch:
diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml
index a6f9e0e7c..361563ad8 100644
--- a/.github/workflows/ios.yml
+++ b/.github/workflows/ios.yml
@@ -19,21 +19,25 @@ jobs:
 
       - uses: actions-rs/toolchain@v1
         with:
-          toolchain: stable
+          toolchain: nightly-2021-11-10
           override: true
 
       - name: install deps
-        run: |
-          rustup target add aarch64-apple-ios x86_64-apple-ios
-          cargo install cargo-lipo
+        run:
           cargo install cbindgen
-
-      - name: build
+      
+      - name: generate C-bindings header file
         run: |
           cd bindings/ergo-lib-c
-          cbindgen --config cbindgen.toml --crate ergo-lib-c --output h/ergo_wallet.h
-          cd ../ergo-lib-ios
-          cargo lipo --release --targets=aarch64-apple-ios,x86_64-apple-ios
-          swift package generate-xcodeproj
-          xcodebuild -project ./ErgoLib.xcodeproj -xcconfig ./Config/Release.xcconfig -sdk iphoneos
-          # xcodebuild -project ./ErgoLib.xcodeproj -xcconfig ./Config/Release.xcconfig -sdk iphonesimulator
+          cbindgen --config cbindgen.toml --crate ergo-lib-c --output h/ergo_lib.h
+
+      - uses: actions-rs/toolchain@v1
+        with:
+          toolchain: stable
+          override: true
+
+      - name: build and run tests
+        run: |
+          cargo build -p ergo-lib-c
+          cd bindings/ergo-lib-ios
+          swift test -Xlinker -L../../target/debug/
diff --git a/.gitignore b/.gitignore
index 7086b058b..cc81f1dd9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ h/
 Packages
 *.xcodeproj
 xcuserdata/
+*.swiftpm
 
 .idea/
 
diff --git a/bindings/ergo-lib-c-core/Cargo.toml b/bindings/ergo-lib-c-core/Cargo.toml
index b3dabe835..4f89cd3b2 100644
--- a/bindings/ergo-lib-c-core/Cargo.toml
+++ b/bindings/ergo-lib-c-core/Cargo.toml
@@ -11,6 +11,17 @@ repository = "https://github.com/ergoplatform/sigma-rust"
 crate-type = ["lib"]
 
 [dependencies]
+base16 = "0.2.1"
+derive_more = "0.99"
 ergo-lib = { version = "^0.14.0", path = "../../ergo-lib" }
+serde = { version = "1.0", features = ["derive"]}
+serde_json = "1.0"
 thiserror = "1"
 
+[dependencies.serde_with]
+version = "1.9.1"
+features = [ "json" ]
+
+[dependencies.bounded-vec]
+version = "^0.5.0"
+features = ["serde"]
diff --git a/bindings/ergo-lib-c-core/src/address.rs b/bindings/ergo-lib-c-core/src/address.rs
new file mode 100644
index 000000000..a696653d9
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/address.rs
@@ -0,0 +1,152 @@
+use crate::{
+    error::*,
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+};
+use ergo_lib::ergotree_ir::chain::address as addr;
+
+/// Address wrapper
+pub struct Address(pub(crate) ergo_lib::ergotree_ir::chain::address::Address);
+pub type AddressPtr = *mut Address;
+pub type ConstAddressPtr = *const Address;
+
+/// Decode (base58) testnet address from string, checking that address is from the testnet
+pub unsafe fn address_from_testnet(
+    address_str: &str,
+    address_out: *mut AddressPtr,
+) -> Result<(), Error> {
+    let address_out = mut_ptr_as_mut(address_out, "address_out")?;
+
+    let encoder = addr::AddressEncoder::new(addr::NetworkPrefix::Testnet);
+    let result = encoder.parse_address_from_str(address_str);
+
+    match result {
+        Ok(address) => {
+            *address_out = Box::into_raw(Box::new(Address(address)));
+            Ok(())
+        }
+        Err(err) => Err(Error::misc(err)),
+    }
+}
+
+/// Decode (base58) mainnet address from string, checking that address is from the mainnet
+pub unsafe fn address_from_mainnet(
+    address_str: &str,
+    address_out: *mut AddressPtr,
+) -> Result<(), Error> {
+    let address_out = mut_ptr_as_mut(address_out, "address_out")?;
+
+    let encoder = addr::AddressEncoder::new(addr::NetworkPrefix::Mainnet);
+    let result = encoder.parse_address_from_str(address_str);
+
+    match result {
+        Ok(address) => {
+            *address_out = Box::into_raw(Box::new(Address(address)));
+            Ok(())
+        }
+        Err(err) => Err(Error::misc(err)),
+    }
+}
+
+/// Decode (base58) address from string without checking the network prefix
+pub unsafe fn address_from_base58(
+    address_str: &str,
+    address_out: *mut AddressPtr,
+) -> Result<(), Error> {
+    let address_out = mut_ptr_as_mut(address_out, "address_out")?;
+    let result = addr::AddressEncoder::unchecked_parse_address_from_str(address_str);
+    match result {
+        Ok(address) => {
+            *address_out = Box::into_raw(Box::new(Address(address)));
+            Ok(())
+        }
+        Err(err) => Err(Error::misc(err)),
+    }
+}
+
+/// Encode address as base58 string
+pub unsafe fn address_to_base58(
+    address: ConstAddressPtr,
+    network_prefix: NetworkPrefix,
+) -> Result<String, Error> {
+    let address = const_ptr_as_ref(address, "address")?;
+    Ok(addr::AddressEncoder::encode_address_as_string(
+        addr::NetworkPrefix::from(network_prefix),
+        &address.0,
+    ))
+}
+
+/// Get the type of the address
+pub unsafe fn address_type_prefix(address: ConstAddressPtr) -> Result<AddressTypePrefix, Error> {
+    let address = const_ptr_as_ref(address, "address")?;
+    Ok(address.0.address_type_prefix().into())
+}
+
+/// Drop the `Address`
+pub fn address_delete(address: AddressPtr) {
+    if !address.is_null() {
+        let boxed = unsafe { Box::from_raw(address) };
+        std::mem::drop(boxed);
+    }
+}
+
+/// Network type
+#[repr(u8)]
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+pub enum NetworkPrefix {
+    /// Mainnet
+    Mainnet = 0,
+    /// Testnet
+    Testnet = 16,
+}
+
+impl From<NetworkPrefix> for addr::NetworkPrefix {
+    fn from(v: NetworkPrefix) -> Self {
+        use addr::NetworkPrefix::*;
+        match v {
+            NetworkPrefix::Mainnet => Mainnet,
+            NetworkPrefix::Testnet => Testnet,
+        }
+    }
+}
+
+impl From<addr::NetworkPrefix> for NetworkPrefix {
+    fn from(v: addr::NetworkPrefix) -> Self {
+        use NetworkPrefix::*;
+        match v {
+            addr::NetworkPrefix::Mainnet => Mainnet,
+            addr::NetworkPrefix::Testnet => Testnet,
+        }
+    }
+}
+
+#[repr(u8)]
+pub enum AddressTypePrefix {
+    /// 0x01 - Pay-to-PublicKey(P2PK) address
+    P2Pk = 1,
+    /// 0x02 - Pay-to-Script-Hash(P2SH)
+    Pay2Sh = 2,
+    /// 0x03 - Pay-to-Script(P2S)
+    Pay2S = 3,
+}
+
+impl From<AddressTypePrefix> for addr::AddressTypePrefix {
+    fn from(v: AddressTypePrefix) -> Self {
+        use addr::AddressTypePrefix::*;
+        match v {
+            AddressTypePrefix::P2Pk => P2Pk,
+            AddressTypePrefix::Pay2Sh => Pay2Sh,
+            AddressTypePrefix::Pay2S => Pay2S,
+        }
+    }
+}
+
+impl From<addr::AddressTypePrefix> for AddressTypePrefix {
+    fn from(v: addr::AddressTypePrefix) -> Self {
+        use AddressTypePrefix::*;
+        match v {
+            addr::AddressTypePrefix::P2Pk => P2Pk,
+            addr::AddressTypePrefix::Pay2Sh => Pay2Sh,
+            addr::AddressTypePrefix::Pay2S => Pay2S,
+        }
+    }
+}
diff --git a/bindings/ergo-lib-c-core/src/block_header.rs b/bindings/ergo-lib-c-core/src/block_header.rs
new file mode 100644
index 000000000..e5163f645
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/block_header.rs
@@ -0,0 +1,21 @@
+//! Block header
+
+use crate::{error::*, util::mut_ptr_as_mut};
+use ergo_lib::ergotree_ir::chain::header::Header;
+
+/// Block header
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct BlockHeader(pub Header);
+pub type BlockHeaderPtr = *mut BlockHeader;
+pub type ConstBlockHeaderPtr = *const BlockHeader;
+
+/// Parse BlockHeader array from JSON (Node API)
+pub unsafe fn block_header_from_json(
+    json: &str,
+    block_header_out: *mut BlockHeaderPtr,
+) -> Result<(), Error> {
+    let block_header_out = mut_ptr_as_mut(block_header_out, "block_header_out")?;
+    let header = serde_json::from_str(json).map(BlockHeader)?;
+    *block_header_out = Box::into_raw(Box::new(header));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/box_builder.rs b/bindings/ergo-lib-c-core/src/box_builder.rs
new file mode 100644
index 000000000..fe4a9b993
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/box_builder.rs
@@ -0,0 +1,200 @@
+use ergo_lib::chain;
+
+use crate::{
+    constant::{ConstConstantPtr, Constant, ConstantPtr},
+    contract::ConstContractPtr,
+    ergo_box::{
+        BoxValue, BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidate, ErgoBoxCandidatePtr,
+        NonMandatoryRegisterId,
+    },
+    token::{ConstTokenAmountPtr, ConstTokenIdPtr, ConstTokenPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// ErgoBoxCandidate builder
+pub struct ErgoBoxCandidateBuilder(chain::ergo_box::box_builder::ErgoBoxCandidateBuilder);
+pub type ErgoBoxCandidateBuilderPtr = *mut ErgoBoxCandidateBuilder;
+pub type ConstErgoBoxCandidateBuilderPtr = *const ErgoBoxCandidateBuilder;
+
+/// Create builder with required box parameters:
+/// `value` - amount of money associated with the box
+/// `contract` - guarding contract([`Contract`]), which should be evaluated to true in order
+/// to open(spend) this box
+/// `creation_height` - height when a transaction containing the box is created.
+/// It should not exceed height of the block, containing the transaction with this box.
+pub unsafe fn ergo_box_candidate_builder_new(
+    value_ptr: ConstBoxValuePtr,
+    contract_ptr: ConstContractPtr,
+    creation_height: u32,
+    builder_out: *mut ErgoBoxCandidateBuilderPtr,
+) -> Result<(), Error> {
+    let value = const_ptr_as_ref(value_ptr, "value_ptr")?;
+    let contract = const_ptr_as_ref(contract_ptr, "contract_ptr")?;
+    let builder_out = mut_ptr_as_mut(builder_out, "builder_out")?;
+    *builder_out = Box::into_raw(Box::new(ErgoBoxCandidateBuilder(
+        chain::ergo_box::box_builder::ErgoBoxCandidateBuilder::new(
+            value.0,
+            contract.0.ergo_tree(),
+            creation_height,
+        ),
+    )));
+    Ok(())
+}
+
+/// Set minimal value (per byte of the serialized box size)
+pub unsafe fn ergo_box_candidate_builder_set_min_box_value_per_byte(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    new_min_value_per_byte: u32,
+) -> Result<(), Error> {
+    let builder_mut = mut_ptr_as_mut(builder_mut, "builder_mut")?;
+    builder_mut
+        .0
+        .set_min_box_value_per_byte(new_min_value_per_byte);
+    Ok(())
+}
+
+/// Get minimal value (per byte of the serialized box size)
+pub unsafe fn ergo_box_candidate_builder_min_box_value_per_byte(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+) -> Result<u32, Error> {
+    let builder = const_ptr_as_ref(builder_ptr, "builder_ptr")?;
+    Ok(builder.0.min_box_value_per_byte())
+}
+
+/// Set new box value
+pub unsafe fn ergo_box_candidate_builder_set_value(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    value_ptr: ConstBoxValuePtr,
+) -> Result<(), Error> {
+    let value = const_ptr_as_ref(value_ptr, "value_ptr")?;
+    let builder_mut = mut_ptr_as_mut(builder_mut, "builder_mut")?;
+    builder_mut.0.set_value(value.0);
+    Ok(())
+}
+
+/// Get box value
+pub unsafe fn ergo_box_candidate_builder_value(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let builder = const_ptr_as_ref(builder_ptr, "builder_ptr")?;
+    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
+    *value_out = Box::into_raw(Box::new(BoxValue(*builder.0.value())));
+    Ok(())
+}
+
+/// Calculate serialized box size(in bytes)
+pub unsafe fn ergo_box_candidate_builder_calc_box_size_bytes(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+) -> Result<usize, Error> {
+    let builder = const_ptr_as_ref(builder_ptr, "builder_ptr")?;
+    let b = builder.0.calc_box_size_bytes()?;
+    Ok(b)
+}
+
+/// Calculate minimal box value for the current box serialized size(in bytes)
+pub unsafe fn ergo_box_candidate_builder_calc_min_box_value(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let builder = const_ptr_as_ref(builder_ptr, "builder_ptr")?;
+    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
+    let value = builder.0.calc_min_box_value().map(BoxValue)?;
+    *value_out = Box::into_raw(Box::new(value));
+    Ok(())
+}
+
+/// Set register with a given id (R4-R9) to the given value
+pub unsafe fn ergo_box_candidate_builder_set_register_value(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    register_id: NonMandatoryRegisterId,
+    constant_ptr: ConstConstantPtr,
+) -> Result<(), Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let builder_mut = mut_ptr_as_mut(builder_mut, "builder_mut")?;
+    builder_mut
+        .0
+        .set_register_value(register_id.into(), constant.0.clone());
+    Ok(())
+}
+
+/// Returns register value for the given register id (R4-R9), or None if the register is empty
+pub unsafe fn ergo_box_candidate_builder_register_value(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    register_id: NonMandatoryRegisterId,
+    constant_out: *mut ConstantPtr,
+) -> Result<bool, Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let builder = const_ptr_as_ref(builder_ptr, "builder_ptr")?;
+    match builder.0.register_value(&register_id.into()) {
+        Some(value) => {
+            *constant_out = Box::into_raw(Box::new(Constant(value.clone())));
+            Ok(true)
+        }
+        None => Ok(false),
+    }
+}
+
+/// Delete register value(make register empty) for the given register id (R4-R9)
+pub unsafe fn ergo_box_candidate_builder_delete_register_value(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    register_id: NonMandatoryRegisterId,
+) -> Result<(), Error> {
+    let builder_mut = mut_ptr_as_mut(builder_mut, "builder_mut")?;
+    builder_mut.0.delete_register_value(&register_id.into());
+    Ok(())
+}
+
+/// Mint token, as defined in <https://github.com/ergoplatform/eips/blob/master/eip-0004.md>
+/// `token` - token id(box id of the first input box in transaction) and token amount,
+/// `token_name` - token name (will be encoded in R4),
+/// `token_desc` - token description (will be encoded in R5),
+/// `num_decimals` - number of decimals (will be encoded in R6)
+pub unsafe fn ergo_box_candidate_builder_mint_token(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    token_ptr: ConstTokenPtr,
+    token_name: &str,
+    token_desc: &str,
+    num_decimals: usize,
+) -> Result<(), Error> {
+    let builder_mut = mut_ptr_as_mut(builder_mut, "builder_mut")?;
+    let token = const_ptr_as_ref(token_ptr, "token_ptr")?;
+    builder_mut.0.mint_token(
+        token.0.clone(),
+        token_name.into(),
+        token_desc.into(),
+        num_decimals,
+    );
+    Ok(())
+}
+
+/// Add given token id and token amount
+pub unsafe fn ergo_box_candidate_builder_add_token(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    token_id_ptr: ConstTokenIdPtr,
+    token_amount_ptr: ConstTokenAmountPtr,
+) -> Result<(), Error> {
+    let builder_mut = mut_ptr_as_mut(builder_mut, "builder_mut")?;
+    let token_id = const_ptr_as_ref(token_id_ptr, "token_id_ptr")?;
+    let token_amount = const_ptr_as_ref(token_amount_ptr, "token_amount_ptr")?;
+    builder_mut
+        .0
+        .add_token(ergo_lib::ergotree_ir::chain::token::Token {
+            token_id: token_id.0.clone(),
+            amount: token_amount.0,
+        });
+    Ok(())
+}
+
+/// Build the box candidate
+pub unsafe fn ergo_box_candidate_builder_build(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    ergo_box_candidate_out: *mut ErgoBoxCandidatePtr,
+) -> Result<(), Error> {
+    let ergo_box_candidate_out = mut_ptr_as_mut(ergo_box_candidate_out, "ergo_box_candidate_out")?;
+    let builder = const_ptr_as_ref(builder_ptr, "builder_ptr")?;
+    let candidate = builder.0.clone().build().map(ErgoBoxCandidate)?;
+    *ergo_box_candidate_out = Box::into_raw(Box::new(candidate));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/box_selector.rs b/bindings/ergo-lib-c-core/src/box_selector.rs
new file mode 100644
index 000000000..fbbf47439
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/box_selector.rs
@@ -0,0 +1,128 @@
+//! Simple box selection algorithms
+use ergo_lib::wallet::{self, box_selector::BoxSelector};
+
+use crate::{
+    collections::{Collection, CollectionPtr, ConstCollectionPtr},
+    ergo_box::{ConstBoxValuePtr, ErgoBox, ErgoBoxAssetsData},
+    token::ConstTokensPtr,
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Selected boxes with change boxes (by [`BoxSelector`])
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct BoxSelection(
+    pub(crate) wallet::box_selector::BoxSelection<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox>,
+);
+pub type BoxSelectionPtr = *mut BoxSelection;
+pub type ConstBoxSelectionPtr = *const BoxSelection;
+
+/// Create a selection to easily inject custom selection algorithms
+pub unsafe fn box_selection_new(
+    ergo_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    change_ergo_boxes_ptr: ConstCollectionPtr<ErgoBoxAssetsData>,
+    box_selection_out: *mut BoxSelectionPtr,
+) -> Result<(), Error> {
+    let ergo_boxes = const_ptr_as_ref(ergo_boxes_ptr, "ergo_boxes_ptr")?;
+    let change_ergo_boxes = const_ptr_as_ref(change_ergo_boxes_ptr, "change_ergo_boxes_ptr")?;
+    let box_selection_out = mut_ptr_as_mut(box_selection_out, "box_selection_out")?;
+    let boxes = wallet::box_selector::SelectedBoxes::from_vec(
+        ergo_boxes.0.clone().into_iter().map(|b| b.0).collect(),
+    )?;
+    *box_selection_out = Box::into_raw(Box::new(BoxSelection(
+        wallet::box_selector::BoxSelection::<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox> {
+            boxes,
+            change_boxes: change_ergo_boxes
+                .0
+                .clone()
+                .into_iter()
+                .map(|b| b.0)
+                .collect(),
+        },
+    )));
+    Ok(())
+}
+
+/// Selected boxes to spend as transaction inputs
+pub unsafe fn box_selection_boxes(
+    box_selection_ptr: ConstBoxSelectionPtr,
+    ergo_boxes_out: *mut CollectionPtr<ErgoBox>,
+) -> Result<(), Error> {
+    let box_selection = const_ptr_as_ref(box_selection_ptr, "box_selection_ptr")?;
+    let ergo_boxes_out = mut_ptr_as_mut(ergo_boxes_out, "ergo_boxes_out")?;
+    *ergo_boxes_out = Box::into_raw(Box::new(Collection(
+        box_selection
+            .0
+            .boxes
+            .clone()
+            .into_iter()
+            .map(ErgoBox)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Selected boxes to use as change
+pub unsafe fn box_selection_change(
+    box_selection_ptr: ConstBoxSelectionPtr,
+    change_ergo_boxes_out: *mut CollectionPtr<ErgoBoxAssetsData>,
+) -> Result<(), Error> {
+    let box_selection = const_ptr_as_ref(box_selection_ptr, "box_selection_ptr")?;
+    let change_ergo_boxes_out = mut_ptr_as_mut(change_ergo_boxes_out, "change_ergo_boxes_out")?;
+    *change_ergo_boxes_out = Box::into_raw(Box::new(Collection(
+        box_selection
+            .0
+            .change_boxes
+            .clone()
+            .into_iter()
+            .map(ErgoBoxAssetsData)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Naive box selector, collects inputs until target balance is reached
+pub struct SimpleBoxSelector(wallet::box_selector::SimpleBoxSelector);
+pub type SimpleBoxSelectorPtr = *mut SimpleBoxSelector;
+pub type ConstSimpleBoxSelectorPtr = *const SimpleBoxSelector;
+
+pub unsafe fn simple_box_selector_new(
+    simple_box_selector_out: *mut SimpleBoxSelectorPtr,
+) -> Result<(), Error> {
+    let simple_box_selector_out =
+        mut_ptr_as_mut(simple_box_selector_out, "simple_box_selector_out")?;
+    *simple_box_selector_out = Box::into_raw(Box::new(SimpleBoxSelector(
+        wallet::box_selector::SimpleBoxSelector::new(),
+    )));
+    Ok(())
+}
+
+/// Selects inputs to satisfy target balance and tokens.
+/// `inputs` - available inputs (returns an error, if empty),
+/// `target_balance` - coins (in nanoERGs) needed,
+/// `target_tokens` - amount of tokens needed.
+/// Returns selected inputs and box assets(value+tokens) with change.
+pub unsafe fn simple_box_selector_select(
+    simple_box_selector_ptr: ConstSimpleBoxSelectorPtr,
+    inputs_ptr: ConstCollectionPtr<ErgoBox>,
+    target_balance_ptr: ConstBoxValuePtr,
+    target_tokens_ptr: ConstTokensPtr,
+    box_selection_out: *mut BoxSelectionPtr,
+) -> Result<(), Error> {
+    let inputs = const_ptr_as_ref(inputs_ptr, "inputs_ptr")?;
+    let target_balance = const_ptr_as_ref(target_balance_ptr, "target_balance_ptr")?;
+    let target_tokens = const_ptr_as_ref(target_tokens_ptr, "target_tokens_ptr")?;
+    let simple_box_selector = const_ptr_as_ref(simple_box_selector_ptr, "simple_box_selector_ptr")?;
+    let box_selection_out = mut_ptr_as_mut(box_selection_out, "box_selection_out")?;
+    let box_selection = simple_box_selector.0.select(
+        inputs.0.clone().into_iter().map(|b| b.0).collect(),
+        target_balance.0,
+        &target_tokens
+            .0
+            .clone()
+            .map(|tokens| tokens.mapped(|t| t.0).as_vec().clone())
+            .unwrap_or_else(Vec::new),
+    )?;
+    *box_selection_out = Box::into_raw(Box::new(BoxSelection(box_selection)));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/collections.rs b/bindings/ergo-lib-c-core/src/collections.rs
new file mode 100644
index 000000000..041d1640b
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/collections.rs
@@ -0,0 +1,50 @@
+//! Collections that can be manipulated from outside Rust
+
+use crate::error::*;
+use crate::util::{const_ptr_as_ref, mut_ptr_as_mut};
+
+pub struct Collection<T>(pub Vec<T>);
+pub type CollectionPtr<T> = *mut Collection<T>;
+pub type ConstCollectionPtr<T> = *const Collection<T>;
+
+pub unsafe fn collection_new<T>(collection_out: *mut CollectionPtr<T>) -> Result<(), Error> {
+    let collection_out = mut_ptr_as_mut(collection_out, "collection_out")?;
+    *collection_out = Box::into_raw(Box::new(Collection(vec![])));
+    Ok(())
+}
+
+pub fn collection_delete<T>(collection: CollectionPtr<T>) {
+    if !collection.is_null() {
+        let boxed = unsafe { Box::from_raw(collection) };
+        std::mem::drop(boxed);
+    }
+}
+
+pub unsafe fn collection_len<T>(collection: ConstCollectionPtr<T>) -> Result<usize, Error> {
+    let collection = const_ptr_as_ref(collection, "collection")?;
+    Ok(collection.0.len())
+}
+
+pub unsafe fn collection_get<T: Clone>(
+    collection: ConstCollectionPtr<T>,
+    index: usize,
+    elem_out: *mut *mut T,
+) -> Result<bool, Error> {
+    let collection = const_ptr_as_ref(collection, "collection")?;
+    let elem_out = mut_ptr_as_mut(elem_out, "elem_out")?;
+    if let Some(elem) = collection.0.get(index) {
+        *elem_out = Box::into_raw(Box::new(elem.clone()));
+        return Ok(true);
+    }
+    Ok(false)
+}
+
+pub unsafe fn collection_add<T: Clone>(
+    collection_out: CollectionPtr<T>,
+    elem: *const T,
+) -> Result<(), Error> {
+    let collection_out = mut_ptr_as_mut(collection_out, "collection_out")?;
+    let elem = const_ptr_as_ref(elem, "elem")?;
+    collection_out.0.push(elem.clone());
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/constant.rs b/bindings/ergo-lib-c-core/src/constant.rs
new file mode 100644
index 000000000..708ef6e48
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/constant.rs
@@ -0,0 +1,142 @@
+//! Ergo constant values
+
+use std::convert::TryFrom;
+
+use crate::{
+    ergo_box::{ConstErgoBoxPtr, ErgoBox, ErgoBoxPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+use ergo_lib::ergotree_ir::{
+    base16_str::Base16Str,
+    chain::base16_bytes::Base16DecodedBytes,
+    mir::constant::{TryExtractFrom, TryExtractInto},
+    serialization::SigmaSerializable,
+    sigma_protocol::{dlog_group::EcPoint, sigma_boolean::ProveDlog},
+};
+
+/// Ergo constant(evaluated) values
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct Constant(pub(crate) ergo_lib::ergotree_ir::mir::constant::Constant);
+pub type ConstantPtr = *mut Constant;
+pub type ConstConstantPtr = *const Constant;
+
+/// Decode from base16 encoded serialized ErgoTree
+pub unsafe fn constant_from_base16_bytes(
+    bytes_str: &str,
+    constant_out: *mut ConstantPtr,
+) -> Result<(), Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let bytes = Base16DecodedBytes::try_from(bytes_str.to_string())?;
+    let constant = ergo_lib::ergotree_ir::mir::constant::Constant::try_from(bytes).map(Constant)?;
+    *constant_out = Box::into_raw(Box::new(constant));
+    Ok(())
+}
+
+/// Encode as Base16-encoded ErgoTree serialized value or return an error if serialization failed
+pub unsafe fn constant_to_base16_str(constant_ptr: ConstConstantPtr) -> Result<String, Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let s = constant.0.base16_str()?;
+    Ok(s)
+}
+
+/// Create from i32 value
+pub unsafe fn constant_from_i32(value: i32, constant_out: *mut ConstantPtr) -> Result<(), Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    *constant_out = Box::into_raw(Box::new(Constant(value.into())));
+    Ok(())
+}
+
+/// Extract i32 value, returning error if wrong type
+pub unsafe fn constant_to_i32(constant_ptr: ConstConstantPtr) -> Result<i32, Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let i = i32::try_extract_from(constant.0.clone())?;
+    Ok(i)
+}
+
+/// Create from i64
+pub unsafe fn constant_from_i64(value: i64, constant_out: *mut ConstantPtr) -> Result<(), Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    *constant_out = Box::into_raw(Box::new(Constant(value.into())));
+    Ok(())
+}
+
+/// Extract i64 value, returning error if wrong type
+pub unsafe fn constant_to_i64(constant_ptr: ConstConstantPtr) -> Result<i64, Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let i = i64::try_extract_from(constant.0.clone())?;
+    Ok(i)
+}
+
+/// Create from byte array
+pub unsafe fn constant_from_bytes(
+    bytes_ptr: *const u8,
+    len: usize,
+    constant_out: *mut ConstantPtr,
+) -> Result<(), Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let bytes = std::slice::from_raw_parts(bytes_ptr, len);
+    *constant_out = Box::into_raw(Box::new(Constant(bytes.to_vec().into())));
+    Ok(())
+}
+
+/// Extract byte array length, returning error if wrong type
+pub unsafe fn constant_bytes_len(constant_ptr: ConstConstantPtr) -> Result<usize, Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let len = Vec::<u8>::try_extract_from(constant.0.clone()).map(|v| v.len())?;
+    Ok(len)
+}
+
+/// Convert to serialized bytes. Key assumption: enough memory has been allocated at the address
+/// pointed-to by `output`. Use `constant_bytes_len` to determine the length of the byte array.
+pub unsafe fn constant_to_bytes(
+    constant_ptr: ConstConstantPtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let src = Vec::<u8>::try_extract_from(constant.0.clone())?;
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, src.len());
+    Ok(())
+}
+
+/// Parse raw [`EcPoint`] value from bytes and make [`ProveDlog`] constant
+pub unsafe fn constant_from_ecpoint_bytes(
+    bytes_ptr: *const u8,
+    len: usize,
+    constant_out: *mut ConstantPtr,
+) -> Result<(), Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let bytes = std::slice::from_raw_parts(bytes_ptr, len);
+    let ecp = EcPoint::sigma_parse_bytes(bytes)?;
+    let c: ergo_lib::ergotree_ir::mir::constant::Constant = ProveDlog::new(ecp).into();
+    *constant_out = Box::into_raw(Box::new(Constant(c)));
+    Ok(())
+}
+
+/// Create from ErgoBox value
+pub unsafe fn constant_from_ergo_box(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    constant_out: *mut ConstantPtr,
+) -> Result<(), Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let c: ergo_lib::ergotree_ir::mir::constant::Constant = ergo_box.0.clone().into();
+    *constant_out = Box::into_raw(Box::new(Constant(c)));
+    Ok(())
+}
+
+/// Extract ErgoBox value, returning error if wrong type
+pub unsafe fn constant_to_ergo_box(
+    constant_ptr: ConstConstantPtr,
+    ergo_box_out: *mut ErgoBoxPtr,
+) -> Result<(), Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let ergo_box_out = mut_ptr_as_mut(ergo_box_out, "ergo_box_out")?;
+    let b = constant
+        .0
+        .clone()
+        .try_extract_into::<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox>()
+        .map(Into::into)?;
+    *ergo_box_out = Box::into_raw(Box::new(ErgoBox(b)));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/context_extension.rs b/bindings/ergo-lib-c-core/src/context_extension.rs
new file mode 100644
index 000000000..bea457dec
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/context_extension.rs
@@ -0,0 +1,43 @@
+use crate::{
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+use ergo_lib::ergotree_interpreter::sigma_protocol::prover;
+
+/// User-defined variables to be put into context
+#[derive(PartialEq, Debug, Clone)]
+pub struct ContextExtension(pub prover::ContextExtension);
+pub type ContextExtensionPtr = *mut ContextExtension;
+pub type ConstContextExtensionPtr = *const ContextExtension;
+
+/// Create new empty ContextExtension instance
+pub unsafe fn context_extension_empty(
+    context_extension_out: *mut ContextExtensionPtr,
+) -> Result<(), Error> {
+    let context_extension_out = mut_ptr_as_mut(context_extension_out, "context_extension_out")?;
+    *context_extension_out = Box::into_raw(Box::new(ContextExtension(
+        prover::ContextExtension::empty(),
+    )));
+    Ok(())
+}
+
+/// Returns the number of elements in the collection
+pub unsafe fn context_extension_len(
+    context_extension_ptr: ConstContextExtensionPtr,
+) -> Result<usize, Error> {
+    let context_extension = const_ptr_as_ref(context_extension_ptr, "context_extension_ptr")?;
+    Ok(context_extension.0.values.len())
+}
+
+/// Returns all keys (represented as u8 values) in the map
+pub unsafe fn context_extension_keys(
+    context_extension_ptr: ConstContextExtensionPtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let context_extension = const_ptr_as_ref(context_extension_ptr, "context_extension_ptr")?;
+    let src: Vec<_> = context_extension.0.values.keys().cloned().collect();
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, src.len());
+    Ok(())
+}
+
+// TODO: get method (needs Constant)
diff --git a/bindings/ergo-lib-c-core/src/contract.rs b/bindings/ergo-lib-c-core/src/contract.rs
new file mode 100644
index 000000000..cc86ac712
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/contract.rs
@@ -0,0 +1,60 @@
+//! Contract, for easier ErgoTree generation
+
+use ergo_lib::{chain, ergoscript_compiler::script_env::ScriptEnv};
+
+use crate::{
+    address::ConstAddressPtr,
+    ergo_tree::{ConstErgoTreePtr, ErgoTree, ErgoTreePtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Defines the contract(script) that will be guarding box contents
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct Contract(pub(crate) chain::contract::Contract);
+pub type ContractPtr = *mut Contract;
+pub type ConstContractPtr = *const Contract;
+
+/// Create new contract from ErgoTree
+pub unsafe fn contract_new(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    contract_out: *mut ContractPtr,
+) -> Result<(), Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let contract_out = mut_ptr_as_mut(contract_out, "contract_out")?;
+    *contract_out = Box::into_raw(Box::new(Contract(chain::contract::Contract::new(
+        ergo_tree.0.clone(),
+    ))));
+    Ok(())
+}
+
+/// Create new contract that allow spending of the guarded box by a given recipient ([`Address`])
+pub unsafe fn contract_pay_to_address(
+    address_ptr: ConstAddressPtr,
+    contract_out: *mut ContractPtr,
+) -> Result<(), Error> {
+    let address = const_ptr_as_ref(address_ptr, "address_ptr")?;
+    let contract_out = mut_ptr_as_mut(contract_out, "contract_out")?;
+    let inner = chain::contract::Contract::pay_to_address(&address.0)?;
+    *contract_out = Box::into_raw(Box::new(Contract(inner)));
+    Ok(())
+}
+
+/// Compiles a contract from ErgoScript source code
+pub unsafe fn contract_compile(source: &str, contract_out: *mut ContractPtr) -> Result<(), Error> {
+    let contract_out = mut_ptr_as_mut(contract_out, "contract_out")?;
+    let inner = chain::contract::Contract::compile(source, ScriptEnv::new())?;
+    *contract_out = Box::into_raw(Box::new(Contract(inner)));
+    Ok(())
+}
+
+/// Get the ErgoTree of the contract
+pub unsafe fn contract_ergo_tree(
+    contract_ptr: ConstContractPtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> Result<(), Error> {
+    let contract = const_ptr_as_ref(contract_ptr, "contract_ptr")?;
+    let ergo_tree_out = mut_ptr_as_mut(ergo_tree_out, "ergo_tree_out")?;
+    *ergo_tree_out = Box::into_raw(Box::new(ErgoTree(contract.0.ergo_tree())));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/data_input.rs b/bindings/ergo-lib-c-core/src/data_input.rs
new file mode 100644
index 000000000..9320a6bb3
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/data_input.rs
@@ -0,0 +1,40 @@
+//! DataInput type
+
+use ergo_lib::chain;
+
+use crate::{
+    ergo_box::{BoxId, BoxIdPtr, ConstBoxIdPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Inputs, that are used to enrich script context, but won't be spent by the transaction
+#[derive(PartialEq, Debug, Clone)]
+pub struct DataInput(pub chain::transaction::DataInput);
+pub type DataInputPtr = *mut DataInput;
+pub type ConstDataInputPtr = *const DataInput;
+
+/// Parse box id (32 byte digest)
+pub unsafe fn data_input_new(
+    box_id_ptr: ConstBoxIdPtr,
+    data_input_out: *mut DataInputPtr,
+) -> Result<(), Error> {
+    let box_id = const_ptr_as_ref(box_id_ptr, "box_id_ptr")?;
+    let data_input_out = mut_ptr_as_mut(data_input_out, "data_input_out")?;
+    *data_input_out = Box::into_raw(Box::new(DataInput(chain::transaction::DataInput {
+        box_id: box_id.0.clone(),
+    })));
+    Ok(())
+}
+
+/// Get box id
+pub unsafe fn data_input_box_id(
+    data_input_ptr: ConstDataInputPtr,
+    box_id_out: *mut BoxIdPtr,
+) -> Result<(), Error> {
+    let box_id_out = mut_ptr_as_mut(box_id_out, "box_id_out")?;
+    let data_input = const_ptr_as_ref(data_input_ptr, "data_input_ptr")?;
+    let box_id = BoxId(data_input.0.box_id.clone());
+    *box_id_out = Box::into_raw(Box::new(box_id));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/ergo_box.rs b/bindings/ergo-lib-c-core/src/ergo_box.rs
new file mode 100644
index 000000000..fccd37704
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/ergo_box.rs
@@ -0,0 +1,428 @@
+//! Box (aka coin, or an unspent output) is a basic concept of a UTXO-based cryptocurrency.
+//! In Bitcoin, such an object is associated with some monetary value (arbitrary,
+//! but with predefined precision, so we use integer arithmetic to work with the value),
+//! and also a guarding script (aka proposition) to protect the box from unauthorized opening.
+//!
+//! In other way, a box is a state element locked by some proposition (ErgoTree).
+//!
+//! In Ergo, box is just a collection of registers, some with mandatory types and semantics,
+//! others could be used by applications in any way.
+//! We add additional fields in addition to amount and proposition~(which stored in the registers R0 and R1).
+//! Namely, register R2 contains additional tokens (a sequence of pairs (token identifier, value)).
+//! Register R3 contains height when block got included into the blockchain and also transaction
+//! identifier and box index in the transaction outputs.
+//! Registers R4-R9 are free for arbitrary usage.
+//!
+//! A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
+//! can not be linked to the same box.
+
+use std::convert::TryFrom;
+
+use ergo_lib::ergotree_ir::chain::{self, ergo_box::NonMandatoryRegisters};
+
+use crate::{
+    constant::{Constant, ConstantPtr},
+    contract::ConstContractPtr,
+    ergo_tree::{ErgoTree, ErgoTreePtr},
+    json::ErgoBoxJsonEip12,
+    token::{ConstTokensPtr, Token, Tokens, TokensPtr},
+    transaction::ConstTxIdPtr,
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Box id (32-byte digest)
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct BoxId(pub chain::ergo_box::BoxId);
+pub type BoxIdPtr = *mut BoxId;
+pub type ConstBoxIdPtr = *const BoxId;
+
+/// Parse box id (32 byte digest) from base16-encoded string
+pub unsafe fn box_id_from_str(box_id_str: &str, box_id_out: *mut BoxIdPtr) -> Result<(), Error> {
+    let box_id_out = mut_ptr_as_mut(box_id_out, "box_id_out")?;
+    let box_id = chain::ergo_box::BoxId::try_from(String::from(box_id_str)).map(BoxId)?;
+    *box_id_out = Box::into_raw(Box::new(box_id));
+    Ok(())
+}
+
+/// Base16 encoded string
+pub unsafe fn box_id_to_str(box_id_ptr: ConstBoxIdPtr) -> Result<String, Error> {
+    let box_id_ptr = const_ptr_as_ref(box_id_ptr, "box_id_ptr")?;
+    Ok(box_id_ptr.0.clone().into())
+}
+
+/// Convert to serialized bytes. Key assumption: 32 bytes have been allocated at the address
+/// pointed-to by `output`.
+pub unsafe fn box_id_to_bytes(box_id_ptr: ConstBoxIdPtr, output: *mut u8) -> Result<(), Error> {
+    let box_id = const_ptr_as_ref(box_id_ptr, "box_id_ptr")?;
+    let src = box_id.0.as_ref();
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, 32);
+    Ok(())
+}
+
+/// Box value in nanoERGs with bound checks
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct BoxValue(pub chain::ergo_box::box_value::BoxValue);
+pub type BoxValuePtr = *mut BoxValue;
+pub type ConstBoxValuePtr = *const BoxValue;
+
+/// Recommended (safe) minimal box value to use in case box size estimation is unavailable.
+/// Allows box size upto 2777 bytes with current min box value per byte of 360 nanoERGs
+pub unsafe fn box_value_safe_user_min(box_value_out: *mut BoxValuePtr) -> Result<(), Error> {
+    let box_value_out = mut_ptr_as_mut(box_value_out, "box_value_out")?;
+    *box_value_out = Box::into_raw(Box::new(BoxValue(
+        chain::ergo_box::box_value::BoxValue::SAFE_USER_MIN,
+    )));
+    Ok(())
+}
+
+/// Number of units inside one ERGO (i.e. one ERG using nano ERG representation)
+pub fn box_value_units_per_ergo() -> i64 {
+    chain::ergo_box::box_value::BoxValue::UNITS_PER_ERGO as i64
+}
+
+/// Create from i64 with bounds check
+pub unsafe fn box_value_from_i64(
+    amount: i64,
+    box_value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let box_value_out = mut_ptr_as_mut(box_value_out, "box_value_out")?;
+    let inner = chain::ergo_box::box_value::BoxValue::try_from(amount as u64)?;
+    *box_value_out = Box::into_raw(Box::new(BoxValue(inner)));
+    Ok(())
+}
+
+/// Get value as signed 64-bit long
+pub unsafe fn box_value_as_i64(box_value_ptr: ConstBoxValuePtr) -> Result<i64, Error> {
+    let box_value = const_ptr_as_ref(box_value_ptr, "box_value_ptr")?;
+    Ok(i64::from(box_value.0))
+}
+
+/// Create a new box value which is the sum of the arguments, returning error if value is out of
+/// bounds.
+pub unsafe fn box_value_sum_of(
+    box_value0_ptr: ConstBoxValuePtr,
+    box_value1_ptr: ConstBoxValuePtr,
+    sum_of_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let box_value0 = const_ptr_as_ref(box_value0_ptr, "box_value0_ptr")?;
+    let box_value1 = const_ptr_as_ref(box_value1_ptr, "box_value1_ptr")?;
+    let sum_of_out = mut_ptr_as_mut(sum_of_out, "sum_of_out")?;
+    let sum = box_value0
+        .0
+        .as_i64()
+        .checked_add(box_value1.0.as_i64())
+        .ok_or_else(|| Error::Misc("BoxValue.sum_of: sum of i64 overflowed".into()))?;
+    let inner = chain::ergo_box::box_value::BoxValue::try_from(sum as u64)?;
+    *sum_of_out = Box::into_raw(Box::new(BoxValue(inner)));
+    Ok(())
+}
+
+/// Contains the same fields as `ErgoBox`, except for transaction id and index, that will be
+/// calculated after full transaction formation.  Use `ErgoBoxCandidateBuilder` to create an
+/// instance.
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ErgoBoxCandidate(pub(crate) chain::ergo_box::ErgoBoxCandidate);
+pub type ErgoBoxCandidatePtr = *mut ErgoBoxCandidate;
+pub type ConstErgoBoxCandidatePtr = *const ErgoBoxCandidate;
+
+/// Returns value (ErgoTree constant) stored in the register or None if the register is empty
+pub unsafe fn ergo_box_candidate_register_value(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    register_id: NonMandatoryRegisterId,
+    constant_out: *mut ConstantPtr,
+) -> Result<bool, Error> {
+    let candidate = const_ptr_as_ref(ergo_box_candidate_ptr, "ergo_box_candidate_ptr")?;
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    if let Some(c) = candidate.0.additional_registers.get(register_id.into()) {
+        *constant_out = Box::into_raw(Box::new(Constant(c.clone())));
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+/// Get box creation height
+pub unsafe fn ergo_box_candidate_creation_height(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+) -> Result<u32, Error> {
+    let candidate = const_ptr_as_ref(ergo_box_candidate_ptr, "ergo_box_candidate_ptr")?;
+    Ok(candidate.0.creation_height)
+}
+
+/// Get tokens for box
+pub unsafe fn ergo_box_candidate_tokens(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    tokens_out: *mut TokensPtr,
+) -> Result<(), Error> {
+    let candidate = const_ptr_as_ref(ergo_box_candidate_ptr, "ergo_box_candidate_ptr")?;
+    let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
+    *tokens_out = Box::into_raw(Box::new(Tokens(
+        candidate.0.tokens.clone().map(|bv| bv.mapped(Token)),
+    )));
+    Ok(())
+}
+
+/// Get ergo tree for box
+pub unsafe fn ergo_box_candidate_ergo_tree(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> Result<(), Error> {
+    let candidate = const_ptr_as_ref(ergo_box_candidate_ptr, "ergo_box_candidate_ptr")?;
+    let ergo_tree_out = mut_ptr_as_mut(ergo_tree_out, "ergo_tree_out")?;
+    *ergo_tree_out = Box::into_raw(Box::new(ErgoTree(candidate.0.ergo_tree.clone())));
+    Ok(())
+}
+
+/// Get box value in nanoERGs
+pub unsafe fn ergo_box_candidate_box_value(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    box_value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let candidate = const_ptr_as_ref(ergo_box_candidate_ptr, "ergo_box_candidate_ptr")?;
+    let box_value_out = mut_ptr_as_mut(box_value_out, "box_value_out")?;
+    *box_value_out = Box::into_raw(Box::new(BoxValue(candidate.0.value)));
+    Ok(())
+}
+
+/// Ergo box, that is taking part in some transaction on the chain Differs with [`ErgoBoxCandidate`]
+/// by added transaction id and an index in the input of that transaction
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ErgoBox(pub(crate) chain::ergo_box::ErgoBox);
+pub type ErgoBoxPtr = *mut ErgoBox;
+pub type ConstErgoBoxPtr = *const ErgoBox;
+
+/// Make a new box with:
+/// `value` - amount of money associated with the box
+/// `contract` - guarding contract([`Contract`]), which should be evaluated to true in order
+/// to open(spend) this box
+/// `creation_height` - height when a transaction containing the box is created.
+/// `tx_id` - transaction id in which this box was "created" (participated in outputs)
+/// `index` - index (in outputs) in the transaction
+pub unsafe fn ergo_box_new(
+    value_ptr: ConstBoxValuePtr,
+    creation_height: u32,
+    contract_ptr: ConstContractPtr,
+    tx_id_ptr: ConstTxIdPtr,
+    index: u16,
+    tokens_ptr: ConstTokensPtr,
+    ergo_box_out: *mut ErgoBoxPtr,
+) -> Result<(), Error> {
+    let value = const_ptr_as_ref(value_ptr, "value_ptr")?;
+    let contract = const_ptr_as_ref(contract_ptr, "contract_ptr")?;
+    let tx_id = const_ptr_as_ref(tx_id_ptr, "tx_id_ptr")?;
+    let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
+    let ergo_box_out = mut_ptr_as_mut(ergo_box_out, "ergo_box_out")?;
+
+    let chain_ergo_tree = contract.0.ergo_tree();
+    let ergo_box = chain::ergo_box::ErgoBox::new(
+        value.0,
+        chain_ergo_tree,
+        tokens.0.clone().map(|tokens| tokens.mapped(|t| t.0)),
+        NonMandatoryRegisters::empty(),
+        creation_height,
+        tx_id.0.clone(),
+        index,
+    )
+    .map(ErgoBox)?;
+    *ergo_box_out = Box::into_raw(Box::new(ergo_box));
+    Ok(())
+}
+
+/// Get box id
+pub unsafe fn ergo_box_box_id(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    box_id_out: *mut BoxIdPtr,
+) -> Result<(), Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let box_id_out = mut_ptr_as_mut(box_id_out, "box_id_out")?;
+    *box_id_out = Box::into_raw(Box::new(BoxId(ergo_box.0.box_id())));
+    Ok(())
+}
+
+/// Get box creation height
+pub unsafe fn ergo_box_creation_height(ergo_box_ptr: ConstErgoBoxPtr) -> Result<u32, Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    Ok(ergo_box.0.creation_height)
+}
+
+/// Get tokens for box
+pub unsafe fn ergo_box_tokens(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    tokens_out: *mut TokensPtr,
+) -> Result<(), Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
+    *tokens_out = Box::into_raw(Box::new(Tokens(
+        ergo_box.0.tokens.clone().map(|tokens| tokens.mapped(Token)),
+    )));
+    Ok(())
+}
+
+/// Get ergo tree for box
+pub unsafe fn ergo_box_ergo_tree(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> Result<(), Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let ergo_tree_out = mut_ptr_as_mut(ergo_tree_out, "ergo_tree_out")?;
+    *ergo_tree_out = Box::into_raw(Box::new(ErgoTree(ergo_box.0.ergo_tree.clone())));
+    Ok(())
+}
+
+/// Get box value in nanoERGs
+pub unsafe fn ergo_box_value(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    box_value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let box_value_out = mut_ptr_as_mut(box_value_out, "box_value_out")?;
+    *box_value_out = Box::into_raw(Box::new(BoxValue(ergo_box.0.value)));
+    Ok(())
+}
+
+/// Returns value (ErgoTree constant) stored in the register or None if the register is empty
+pub unsafe fn ergo_box_register_value(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    register_id: NonMandatoryRegisterId,
+    constant_out: *mut ConstantPtr,
+) -> Result<bool, Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    if let Some(c) = ergo_box.0.additional_registers.get(register_id.into()) {
+        *constant_out = Box::into_raw(Box::new(Constant(c.clone())));
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+/// Parse from JSON.  Supports Ergo Node/Explorer API and box values and token amount encoded as
+/// strings
+pub unsafe fn ergo_box_from_json(json: &str, ergo_box_out: *mut ErgoBoxPtr) -> Result<(), Error> {
+    let ergo_box_out = mut_ptr_as_mut(ergo_box_out, "ergo_box_out")?;
+    let unsigned_tx = serde_json::from_str(json).map(ErgoBox)?;
+    *ergo_box_out = Box::into_raw(Box::new(unsigned_tx));
+    Ok(())
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+pub unsafe fn ergo_box_to_json(ergo_box_ptr: ConstErgoBoxPtr) -> Result<String, Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let s = serde_json::to_string(&ergo_box.0)?;
+    Ok(s)
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+pub unsafe fn ergo_box_to_json_eip12(ergo_box_ptr: ConstErgoBoxPtr) -> Result<String, Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let box_dapp: ErgoBoxJsonEip12 = ergo_box.0.clone().into();
+    let s = serde_json::to_string(&box_dapp)?;
+    Ok(s)
+}
+
+/// Pair of <value, tokens> for a box
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ErgoBoxAssetsData(pub(crate) ergo_lib::wallet::box_selector::ErgoBoxAssetsData);
+pub type ErgoBoxAssetsDataPtr = *mut ErgoBoxAssetsData;
+pub type ConstErgoBoxAssetsDataPtr = *const ErgoBoxAssetsData;
+
+/// Create new instance
+pub unsafe fn ergo_box_assets_data_new(
+    value_ptr: ConstBoxValuePtr,
+    tokens_ptr: ConstTokensPtr,
+    ergo_box_assets_data_out: *mut ErgoBoxAssetsDataPtr,
+) -> Result<(), Error> {
+    let value = const_ptr_as_ref(value_ptr, "value_ptr")?;
+    let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
+    let ergo_box_assets_data_out =
+        mut_ptr_as_mut(ergo_box_assets_data_out, "ergo_box_assets_data_out")?;
+    let tokens = tokens.0.clone().map(|tokens| tokens.mapped(|t| t.0));
+    *ergo_box_assets_data_out = Box::into_raw(Box::new(ErgoBoxAssetsData(
+        ergo_lib::wallet::box_selector::ErgoBoxAssetsData {
+            value: value.0,
+            tokens,
+        },
+    )));
+    Ok(())
+}
+
+/// Value part of the box
+pub unsafe fn ergo_box_assets_data_value(
+    ergo_box_assets_data_ptr: ConstErgoBoxAssetsDataPtr,
+    value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let ergo_box_assets_data =
+        const_ptr_as_ref(ergo_box_assets_data_ptr, "ergo_box_assets_data_ptr")?;
+    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
+
+    *value_out = Box::into_raw(Box::new(BoxValue(ergo_box_assets_data.0.value)));
+    Ok(())
+}
+
+/// Tokens part of the box
+pub unsafe fn ergo_box_assets_data_tokens(
+    ergo_box_assets_data_ptr: ConstErgoBoxAssetsDataPtr,
+    tokens_out: *mut TokensPtr,
+) -> Result<(), Error> {
+    let ergo_box_assets_data =
+        const_ptr_as_ref(ergo_box_assets_data_ptr, "ergo_box_assets_data_ptr")?;
+    let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
+
+    *tokens_out = Box::into_raw(Box::new(Tokens(
+        ergo_box_assets_data
+            .0
+            .tokens
+            .clone()
+            .map(|tokens| tokens.mapped(Token)),
+    )));
+    Ok(())
+}
+
+/// Type for representing box registers R4 - R9
+#[repr(u8)]
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub enum NonMandatoryRegisterId {
+    /// id for R4 register
+    R4 = 4,
+    /// id for R5 register
+    R5 = 5,
+    /// id for R6 register
+    R6 = 6,
+    /// id for R7 register
+    R7 = 7,
+    /// id for R8 register
+    R8 = 8,
+    /// id for R9 register
+    R9 = 9,
+}
+
+impl NonMandatoryRegisterId {}
+
+impl From<NonMandatoryRegisterId> for chain::ergo_box::NonMandatoryRegisterId {
+    fn from(v: NonMandatoryRegisterId) -> Self {
+        use chain::ergo_box::NonMandatoryRegisterId::*;
+        match v {
+            NonMandatoryRegisterId::R4 => R4,
+            NonMandatoryRegisterId::R5 => R5,
+            NonMandatoryRegisterId::R6 => R6,
+            NonMandatoryRegisterId::R7 => R7,
+            NonMandatoryRegisterId::R8 => R8,
+            NonMandatoryRegisterId::R9 => R9,
+        }
+    }
+}
+
+impl From<chain::ergo_box::NonMandatoryRegisterId> for NonMandatoryRegisterId {
+    fn from(v: chain::ergo_box::NonMandatoryRegisterId) -> Self {
+        use NonMandatoryRegisterId::*;
+        match v {
+            chain::ergo_box::NonMandatoryRegisterId::R4 => R4,
+            chain::ergo_box::NonMandatoryRegisterId::R5 => R5,
+            chain::ergo_box::NonMandatoryRegisterId::R6 => R6,
+            chain::ergo_box::NonMandatoryRegisterId::R7 => R7,
+            chain::ergo_box::NonMandatoryRegisterId::R8 => R8,
+            chain::ergo_box::NonMandatoryRegisterId::R9 => R9,
+        }
+    }
+}
diff --git a/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs b/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs
new file mode 100644
index 000000000..c8e270973
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/ergo_state_ctx.rs
@@ -0,0 +1,53 @@
+//! Ergo blockchain state (for ErgoTree evaluation)
+use ergo_lib::chain;
+
+use crate::block_header::BlockHeader;
+use crate::collections::ConstCollectionPtr;
+use crate::header::PreHeader;
+use crate::util::const_ptr_as_ref;
+use crate::Error;
+use std::convert::TryInto;
+
+/// Blockchain state (last headers, etc.)
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ErgoStateContext(pub(crate) chain::ergo_state_context::ErgoStateContext);
+pub type ErgoStateContextPtr = *mut ErgoStateContext;
+pub type ConstErgoStateContextPtr = *const ErgoStateContext;
+
+/// Create new context from pre-header
+pub unsafe fn ergo_state_context_new(
+    pre_header_ptr: *const PreHeader,
+    headers: ConstCollectionPtr<BlockHeader>,
+    ergo_state_context_out: *mut ErgoStateContextPtr,
+) -> Result<(), Error> {
+    let pre_header = const_ptr_as_ref(pre_header_ptr, "pre_header_ptr")?;
+    let headers = const_ptr_as_ref(headers, "headers")?;
+    match headers.0.len() {
+        10 => {
+            *ergo_state_context_out = Box::into_raw(Box::new(ErgoStateContext(
+                chain::ergo_state_context::ErgoStateContext::new(
+                    pre_header.clone().0,
+                    headers
+                        .0
+                        .clone()
+                        .into_iter()
+                        .map(|x| x.0)
+                        .collect::<Vec<_>>()
+                        .try_into()
+                        .unwrap(),
+                ),
+            )));
+            Ok(())
+        }
+        h => Err(Error::Misc(
+            format!("Not enough block headers, expected 10 but got {}", h).into(),
+        )),
+    }
+}
+
+pub fn ergo_state_context_delete(header: ErgoStateContextPtr) {
+    if !header.is_null() {
+        let boxed = unsafe { Box::from_raw(header) };
+        std::mem::drop(boxed);
+    }
+}
diff --git a/bindings/ergo-lib-c-core/src/ergo_tree.rs b/bindings/ergo-lib-c-core/src/ergo_tree.rs
new file mode 100644
index 000000000..feedb0af7
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/ergo_tree.rs
@@ -0,0 +1,143 @@
+//! ErgoTree
+
+use std::convert::TryFrom;
+
+use ergo_lib::ergotree_ir::{
+    chain::base16_bytes::Base16DecodedBytes, serialization::SigmaSerializable,
+};
+
+use crate::{
+    constant::{ConstConstantPtr, Constant, ConstantPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// The root of ErgoScript IR. Serialized instances of this class are self sufficient and can be passed around.
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ErgoTree(pub(crate) ergo_lib::ergotree_ir::ergo_tree::ErgoTree);
+pub type ErgoTreePtr = *mut ErgoTree;
+pub type ConstErgoTreePtr = *const ErgoTree;
+
+/// Decode from base16 encoded serialized ErgoTree
+pub unsafe fn ergo_tree_from_base16_bytes(
+    bytes_str: &str,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> Result<(), Error> {
+    let ergo_tree_out = mut_ptr_as_mut(ergo_tree_out, "ergo_tree_out")?;
+    let bytes = Base16DecodedBytes::try_from(bytes_str.to_string())?.0;
+    let ergo_tree =
+        ergo_lib::ergotree_ir::ergo_tree::ErgoTree::sigma_parse_bytes(&bytes).map(ErgoTree)?;
+    *ergo_tree_out = Box::into_raw(Box::new(ergo_tree));
+    Ok(())
+}
+
+/// Decode from encoded serialized ErgoTree
+pub unsafe fn ergo_tree_from_bytes(
+    bytes_ptr: *const u8,
+    len: usize,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> Result<(), Error> {
+    if bytes_ptr.is_null() {
+        return Err(Error::Misc("bytes_ptr is null".into()));
+    }
+    let bytes = std::slice::from_raw_parts(bytes_ptr, len);
+    let ergo_tree_out = mut_ptr_as_mut(ergo_tree_out, "ergo_tree_out")?;
+    let ergo_tree =
+        ergo_lib::ergotree_ir::ergo_tree::ErgoTree::sigma_parse_bytes(bytes).map(ErgoTree)?;
+    *ergo_tree_out = Box::into_raw(Box::new(ergo_tree));
+    Ok(())
+}
+
+/// Return length of the `&[u8]` serialized representation of `ErgoTree`.
+pub unsafe fn ergo_tree_bytes_len(ergo_tree_ptr: ConstErgoTreePtr) -> Result<usize, Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let len = ergo_tree.0.sigma_serialize_bytes().map(|v| v.len())?;
+    Ok(len)
+}
+
+/// Convert to serialized bytes. Key assumption: enough memory has been allocated at the address
+/// pointed-to by `output`. Use `ergo_tree_bytes_len` to determine the length of the byte array.
+pub unsafe fn ergo_tree_to_bytes(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let src = ergo_tree.0.sigma_serialize_bytes()?;
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, src.len());
+    Ok(())
+}
+
+/// Convert to base16-encoded serialized bytes
+pub unsafe fn ergo_tree_to_base16_bytes(ergo_tree_ptr: ConstErgoTreePtr) -> Result<String, Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let s = ergo_tree.0.to_base16_bytes()?;
+    Ok(s)
+}
+
+/// Returns constants number as stored in serialized ErgoTree or error if the parsing of constants
+/// failed
+pub unsafe fn ergo_tree_constants_len(ergo_tree_ptr: ConstErgoTreePtr) -> Result<usize, Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let len = ergo_tree.0.constants_len()?;
+    Ok(len)
+}
+
+/// If constant with given index (as stored in serialized ErgoTree) exists, allocate it and store in
+/// `constant_out` and return `Ok(true)`. If constant doesn't exist at the given index return
+/// Ok(false).  If parsing failed then return error.
+pub unsafe fn ergo_tree_get_constant(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    index: usize,
+    constant_out: *mut ConstantPtr,
+) -> Result<bool, Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let constant = ergo_tree.0.get_constant(index).map(|c| c.map(Constant))?;
+    if let Some(constant) = constant {
+        *constant_out = Box::into_raw(Box::new(constant));
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+/// Returns new ErgoTree instance with a new constant value for a given index in constants list (as
+/// stored in serialized ErgoTree), or an error. Note that the original ErgoTree instance
+/// pointed-at by `ergo_tree_ptr` is untouched.
+pub unsafe fn ergo_tree_with_constant(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    index: usize,
+    constant_ptr: ConstConstantPtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> Result<(), Error> {
+    let ergo_tree_cloned = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?.clone();
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let ergo_tree_out = mut_ptr_as_mut(ergo_tree_out, "ergo_tree_out")?;
+    let new_inner = ergo_tree_cloned
+        .0
+        .with_constant(index, constant.0.clone())?;
+    *ergo_tree_out = Box::into_raw(Box::new(ErgoTree(new_inner)));
+    Ok(())
+}
+
+/// Return length of the `&[u8]` serialized representation of `ErgoTree` template.
+pub unsafe fn ergo_tree_template_bytes_len(
+    ergo_tree_ptr: ConstErgoTreePtr,
+) -> Result<usize, Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let len = ergo_tree.0.template_bytes().map(|v| v.len())?;
+    Ok(len)
+}
+
+/// Serialized proposition expression of SigmaProp type with ConstantPlaceholder nodes instead of
+/// Constant nodes. Key assumption: enough memory has been allocated at the address pointed-to by
+/// `output`. Use `ergo_tree_template_bytes_len` to determine the length of the byte array.
+pub unsafe fn ergo_tree_template_bytes(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let ergo_tree = const_ptr_as_ref(ergo_tree_ptr, "ergo_tree_ptr")?;
+    let src = ergo_tree.0.template_bytes()?;
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, src.len());
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/error_conversion.rs b/bindings/ergo-lib-c-core/src/error_conversion.rs
new file mode 100644
index 000000000..83bc1b1bd
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/error_conversion.rs
@@ -0,0 +1,72 @@
+//! Code to simplify conversion of `ergo_lib` errors into local `Error`.
+
+use std::array::TryFromSliceError;
+use std::num::ParseIntError;
+
+use base16::DecodeError;
+use bounded_vec::BoundedVecOutOfBounds;
+use ergo_lib::chain::transaction::TransactionError;
+use ergo_lib::ergoscript_compiler::compiler::CompileError;
+use ergo_lib::ergotree_ir::chain::address::AddressEncoderError;
+use ergo_lib::ergotree_ir::chain::address::AddressError;
+use ergo_lib::ergotree_ir::chain::digest32::Digest32Error;
+use ergo_lib::ergotree_ir::chain::ergo_box::box_value::BoxValueError;
+use ergo_lib::ergotree_ir::chain::token::TokenAmountError;
+use ergo_lib::wallet::derivation_path::ChildIndexError;
+use ergo_lib::wallet::signing::TxSigningError;
+use ergo_lib::{
+    chain::ergo_box::box_builder::ErgoBoxCandidateBuilderError,
+    ergotree_ir::{
+        ergo_tree::{ErgoTreeConstantError, ErgoTreeConstantsParsingError, ErgoTreeError},
+        mir::constant::TryExtractFromError,
+        serialization::{SigmaParsingError, SigmaSerializationError},
+    },
+    wallet::{box_selector::BoxSelectorError, tx_builder::TxBuilderError, WalletError},
+};
+use serde_json::error::Error as SerdeError;
+
+macro_rules! convert_error {
+    ($t:ident) => {
+        impl std::convert::From<$t> for crate::Error {
+            fn from(e: $t) -> Self {
+                crate::Error::Misc(format!("{}", e).into())
+            }
+        }
+    };
+}
+
+convert_error!(AddressError);
+convert_error!(SigmaParsingError);
+convert_error!(AddressEncoderError);
+convert_error!(ErgoBoxCandidateBuilderError);
+convert_error!(TryExtractFromError);
+convert_error!(BoxSelectorError);
+convert_error!(Digest32Error);
+convert_error!(SigmaSerializationError);
+convert_error!(SerdeError);
+convert_error!(BoxValueError);
+convert_error!(TokenAmountError);
+convert_error!(TxBuilderError);
+convert_error!(TxSigningError);
+convert_error!(WalletError);
+convert_error!(DecodeError);
+convert_error!(TryFromSliceError);
+
+macro_rules! convert_error_via_debug {
+    ($t:ident) => {
+        impl std::convert::From<$t> for crate::Error {
+            fn from(e: $t) -> Self {
+                crate::Error::Misc(format!("{:?}", e).into())
+            }
+        }
+    };
+}
+
+convert_error_via_debug!(CompileError);
+convert_error_via_debug!(ErgoTreeError);
+convert_error_via_debug!(ErgoTreeConstantError);
+convert_error_via_debug!(ErgoTreeConstantsParsingError);
+convert_error_via_debug!(ParseIntError);
+convert_error_via_debug!(ChildIndexError);
+convert_error_via_debug!(BoundedVecOutOfBounds);
+convert_error_via_debug!(TransactionError);
diff --git a/bindings/ergo-lib-c-core/src/header.rs b/bindings/ergo-lib-c-core/src/header.rs
new file mode 100644
index 000000000..cfa377f01
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/header.rs
@@ -0,0 +1,22 @@
+//! Block header with the current `spendingTransaction`, that can be predicted by a miner before it's formation
+
+use crate::{block_header::ConstBlockHeaderPtr, error::Error, util::const_ptr_as_ref};
+
+/// Block header with the current `spendingTransaction`, that can be predicted
+/// by a miner before its formation
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct PreHeader(pub ergo_lib::ergotree_ir::chain::preheader::PreHeader);
+pub type PreHeaderPtr = *mut PreHeader;
+pub type ConstPreHeaderPtr = *const PreHeader;
+
+/// Create instance using data from block header
+pub unsafe fn preheader_from_block_header(
+    block_header_ptr: ConstBlockHeaderPtr,
+    preheader_out: *mut PreHeaderPtr,
+) -> Result<(), Error> {
+    let block_header = const_ptr_as_ref(block_header_ptr, "block_header")?;
+    let bh: ergo_lib::ergotree_ir::chain::header::Header = block_header.0.clone();
+    let ph: ergo_lib::ergotree_ir::chain::preheader::PreHeader = bh.into();
+    *preheader_out = Box::into_raw(Box::new(PreHeader(ph)));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/input.rs b/bindings/ergo-lib-c-core/src/input.rs
new file mode 100644
index 000000000..d9b8c7ddc
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/input.rs
@@ -0,0 +1,122 @@
+//! Ergo input
+
+use ergo_lib::{chain, ergotree_interpreter::sigma_protocol::prover::ProofBytes};
+
+use crate::{
+    context_extension::{ContextExtension, ContextExtensionPtr},
+    ergo_box::{BoxId, BoxIdPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Unsigned inputs used in constructing unsigned transactions
+#[derive(PartialEq, Debug, Clone)]
+pub struct UnsignedInput(pub chain::transaction::UnsignedInput);
+pub type UnsignedInputPtr = *mut UnsignedInput;
+pub type ConstUnsignedInputPtr = *const UnsignedInput;
+
+/// Get box id
+pub unsafe fn unsigned_input_box_id(
+    unsigned_input_ptr: ConstUnsignedInputPtr,
+    box_id_out: *mut BoxIdPtr,
+) -> Result<(), Error> {
+    let box_id_out = mut_ptr_as_mut(box_id_out, "box_id_out")?;
+    let unsigned_input = const_ptr_as_ref(unsigned_input_ptr, "unsigned_input_ptr")?;
+    let box_id = BoxId(unsigned_input.0.box_id.clone());
+    *box_id_out = Box::into_raw(Box::new(box_id));
+    Ok(())
+}
+
+/// Get extension
+pub unsafe fn unsigned_input_context_extension(
+    unsigned_input_ptr: ConstUnsignedInputPtr,
+    context_extension_out: *mut ContextExtensionPtr,
+) -> Result<(), Error> {
+    let context_extension_out = mut_ptr_as_mut(context_extension_out, "context_extension_out")?;
+    let unsigned_input = const_ptr_as_ref(unsigned_input_ptr, "unsigned_input_ptr")?;
+    let context_extension = ContextExtension(unsigned_input.0.extension.clone());
+    *context_extension_out = Box::into_raw(Box::new(context_extension));
+    Ok(())
+}
+
+/// Signed inputs used in signed transactions
+#[derive(PartialEq, Debug, Clone)]
+pub struct Input(pub(crate) chain::transaction::Input);
+pub type InputPtr = *mut Input;
+pub type ConstInputPtr = *const Input;
+
+/// Get box id
+pub unsafe fn input_box_id(
+    input_ptr: ConstInputPtr,
+    box_id_out: *mut BoxIdPtr,
+) -> Result<(), Error> {
+    let box_id_out = mut_ptr_as_mut(box_id_out, "box_id_out")?;
+    let input = const_ptr_as_ref(input_ptr, "input_ptr")?;
+    let box_id = BoxId(input.0.box_id.clone());
+    *box_id_out = Box::into_raw(Box::new(box_id));
+    Ok(())
+}
+
+/// Get the spending proof
+pub unsafe fn input_spending_proof(
+    input_ptr: ConstInputPtr,
+    prover_result_out: *mut ProverResultPtr,
+) -> Result<(), Error> {
+    let prover_result_out = mut_ptr_as_mut(prover_result_out, "prover_result_out")?;
+    let input = const_ptr_as_ref(input_ptr, "input_ptr")?;
+    let prover_result = ProverResult(input.0.spending_proof.clone());
+    *prover_result_out = Box::into_raw(Box::new(prover_result));
+    Ok(())
+}
+
+/// Proof of correctness of tx spending
+#[derive(PartialEq, Debug, Clone)]
+pub struct ProverResult(chain::transaction::input::prover_result::ProverResult);
+pub type ProverResultPtr = *mut ProverResult;
+pub type ConstProverResultPtr = *const ProverResult;
+
+/// Return the number of bytes that make up the proof.
+pub unsafe fn prover_result_proof_len(
+    prover_result_ptr: ConstProverResultPtr,
+) -> Result<usize, Error> {
+    let prover_result = const_ptr_as_ref(prover_result_ptr, "prover_result_ptr")?;
+    Ok(match &prover_result.0.proof {
+        ProofBytes::Some(b) => b.len(),
+        ProofBytes::Empty => 0,
+    })
+}
+
+/// Get proof. Key assumption: enough memory has been allocated at the address pointed-to by
+/// `output`. Use `prover_result_proof_len` to determine the length of the byte
+/// array.
+pub unsafe fn prover_result_proof(
+    prover_result_ptr: ConstProverResultPtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let prover_result = const_ptr_as_ref(prover_result_ptr, "prover_result_ptr")?;
+    let src: Vec<_> = prover_result.0.proof.clone().into();
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, src.len());
+    Ok(())
+}
+
+/// Get extension
+pub unsafe fn prover_result_context_extension(
+    prover_result_ptr: ConstProverResultPtr,
+    context_extension_out: *mut ContextExtensionPtr,
+) -> Result<(), Error> {
+    let prover_result = const_ptr_as_ref(prover_result_ptr, "prover_result_ptr")?;
+    let context_extension_out = mut_ptr_as_mut(context_extension_out, "context_extension_out")?;
+    *context_extension_out = Box::into_raw(Box::new(ContextExtension(
+        prover_result.0.extension.clone(),
+    )));
+    Ok(())
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+pub unsafe fn prover_result_to_json(
+    prover_result_ptr: ConstProverResultPtr,
+) -> Result<String, Error> {
+    let prover_result = const_ptr_as_ref(prover_result_ptr, "prover_result_ptr")?;
+    let s = serde_json::to_string_pretty(&prover_result.0.clone())?;
+    Ok(s)
+}
diff --git a/bindings/ergo-lib-c-core/src/json.rs b/bindings/ergo-lib-c-core/src/json.rs
new file mode 100644
index 000000000..9c4007cdf
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/json.rs
@@ -0,0 +1,203 @@
+//! JSON serialization according to EIP-12 (using strings for BoxValue and TokenAmount)
+
+use std::convert::TryInto;
+
+use derive_more::FromStr;
+use ergo_lib::chain::transaction::unsigned::UnsignedTransaction;
+use ergo_lib::chain::transaction::DataInput;
+use ergo_lib::chain::transaction::Input;
+use ergo_lib::chain::transaction::Transaction;
+use ergo_lib::chain::transaction::TxId;
+use ergo_lib::chain::transaction::UnsignedInput;
+use ergo_lib::ergotree_ir::chain::ergo_box::box_value::BoxValue;
+use ergo_lib::ergotree_ir::chain::ergo_box::BoxId;
+use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
+use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBoxCandidate;
+use ergo_lib::ergotree_ir::chain::ergo_box::NonMandatoryRegisters;
+use ergo_lib::ergotree_ir::chain::token::Token;
+use ergo_lib::ergotree_ir::chain::token::TokenId;
+use ergo_lib::ergotree_ir::ergo_tree::ErgoTree;
+use serde::Serialize;
+
+#[derive(Serialize, PartialEq, Debug, Clone)]
+pub(crate) struct TransactionJsonEip12 {
+    #[cfg_attr(feature = "json", serde(rename = "id"))]
+    pub tx_id: TxId,
+    /// inputs, that will be spent by this transaction.
+    #[cfg_attr(feature = "json", serde(rename = "inputs"))]
+    pub inputs: Vec<Input>,
+    /// inputs, that are not going to be spent by transaction, but will be reachable from inputs
+    /// scripts. `dataInputs` scripts will not be executed, thus their scripts costs are not
+    /// included in transaction cost and they do not contain spending proofs.
+    #[cfg_attr(feature = "json", serde(rename = "dataInputs"))]
+    pub data_inputs: Vec<DataInput>,
+    #[cfg_attr(feature = "json", serde(rename = "outputs"))]
+    pub outputs: Vec<ErgoBoxJsonEip12>,
+}
+
+impl From<Transaction> for TransactionJsonEip12 {
+    fn from(t: Transaction) -> Self {
+        TransactionJsonEip12 {
+            tx_id: t.id(),
+            inputs: t.inputs.try_into().unwrap(),
+            data_inputs: t
+                .data_inputs
+                .map(|di| di.try_into().unwrap())
+                .unwrap_or_else(Vec::new),
+            outputs: t.outputs.into_iter().map(|b| b.into()).collect(),
+        }
+    }
+}
+
+#[derive(Serialize, PartialEq, Debug, Clone)]
+pub(crate) struct UnsignedTransactionJsonEip12 {
+    /// unsigned inputs, that will be spent by this transaction.
+    #[cfg_attr(feature = "json", serde(rename = "inputs"))]
+    pub inputs: Vec<UnsignedInput>,
+    /// inputs, that are not going to be spent by transaction, but will be reachable from inputs
+    /// scripts. `dataInputs` scripts will not be executed, thus their scripts costs are not
+    /// included in transaction cost and they do not contain spending proofs.
+    #[cfg_attr(feature = "json", serde(rename = "dataInputs"))]
+    pub data_inputs: Vec<DataInput>,
+    /// box candidates to be created by this transaction
+    #[cfg_attr(feature = "json", serde(rename = "outputs"))]
+    pub outputs: Vec<ErgoBoxCandidateJsonEip12>,
+}
+
+impl From<UnsignedTransaction> for UnsignedTransactionJsonEip12 {
+    fn from(t: UnsignedTransaction) -> Self {
+        UnsignedTransactionJsonEip12 {
+            inputs: t.inputs.try_into().unwrap(),
+            data_inputs: t
+                .data_inputs
+                .map(|di| di.try_into().unwrap())
+                .unwrap_or_else(Vec::new),
+            outputs: t.output_candidates.into_iter().map(|b| b.into()).collect(),
+        }
+    }
+}
+
+#[derive(Serialize, PartialEq, Eq, Debug, Clone)]
+pub(crate) struct ErgoBoxJsonEip12 {
+    #[serde(rename = "boxId", alias = "id")]
+    pub box_id: Option<BoxId>,
+    /// amount of money associated with the box
+    #[serde(rename = "value")]
+    pub value: BoxValueJsonEip12,
+    /// guarding script, which should be evaluated to true in order to open this box
+    #[serde(
+        rename = "ergoTree",
+        with = "ergo_lib::ergotree_ir::chain::json::ergo_tree"
+    )]
+    pub ergo_tree: ErgoTree,
+    /// secondary tokens the box contains
+    #[serde(rename = "assets")]
+    pub tokens: Vec<TokenJsonEip12>,
+    ///  additional registers the box can carry over
+    #[serde(rename = "additionalRegisters")]
+    pub additional_registers: NonMandatoryRegisters,
+    /// height when a transaction containing the box was created.
+    /// This height is declared by user and should not exceed height of the block,
+    /// containing the transaction with this box.
+    #[serde(rename = "creationHeight")]
+    pub creation_height: u32,
+    /// id of transaction which created the box
+    #[serde(rename = "transactionId", alias = "txId")]
+    pub transaction_id: TxId,
+    /// number of box (from 0 to total number of boxes the transaction with transactionId created - 1)
+    #[serde(rename = "index")]
+    pub index: u16,
+}
+
+impl From<ErgoBox> for ErgoBoxJsonEip12 {
+    fn from(b: ErgoBox) -> Self {
+        ErgoBoxJsonEip12 {
+            box_id: b.box_id().into(),
+            value: b.value.into(),
+            ergo_tree: b.ergo_tree,
+            tokens: b.tokens.into_iter().flatten().map(|t| t.into()).collect(),
+            additional_registers: b.additional_registers,
+            creation_height: b.creation_height,
+            transaction_id: b.transaction_id,
+            index: b.index,
+        }
+    }
+}
+
+/// Contains the same fields as `ErgoBox`, except if transaction id and index,
+/// that will be calculated after full transaction formation.
+#[derive(Serialize, PartialEq, Eq, Clone, Debug)]
+pub(crate) struct ErgoBoxCandidateJsonEip12 {
+    /// amount of money associated with the box
+    #[serde(rename = "value")]
+    pub value: BoxValueJsonEip12,
+    /// guarding script, which should be evaluated to true in order to open this box
+    #[serde(
+        rename = "ergoTree",
+        with = "ergo_lib::ergotree_ir::chain::json::ergo_tree"
+    )]
+    pub ergo_tree: ErgoTree,
+    #[serde(rename = "assets")]
+    pub tokens: Vec<TokenJsonEip12>,
+    ///  additional registers the box can carry over
+    #[serde(rename = "additionalRegisters")]
+    pub additional_registers: NonMandatoryRegisters,
+    /// height when a transaction containing the box was created.
+    /// This height is declared by user and should not exceed height of the block,
+    /// containing the transaction with this box.
+    #[serde(rename = "creationHeight")]
+    pub creation_height: u32,
+}
+
+impl From<ErgoBoxCandidate> for ErgoBoxCandidateJsonEip12 {
+    fn from(b: ErgoBoxCandidate) -> Self {
+        ErgoBoxCandidateJsonEip12 {
+            value: b.value.into(),
+            ergo_tree: b.ergo_tree,
+            tokens: b.tokens.into_iter().flatten().map(|t| t.into()).collect(),
+            additional_registers: b.additional_registers,
+            creation_height: b.creation_height,
+        }
+    }
+}
+
+#[serde_with::serde_as]
+#[derive(
+    serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Debug, Clone, Copy, FromStr,
+)]
+/// Box value in nanoERGs with bound checks
+pub(crate) struct BoxValueJsonEip12(#[serde_as(as = "serde_with::DisplayFromStr")] u64);
+
+impl From<BoxValue> for BoxValueJsonEip12 {
+    fn from(bv: BoxValue) -> Self {
+        BoxValueJsonEip12(*bv.as_u64())
+    }
+}
+
+/// Token represented with token id paired with it's amount
+#[derive(Serialize, PartialEq, Eq, Debug, Clone)]
+pub struct TokenJsonEip12 {
+    /// token id
+    #[serde(rename = "tokenId")]
+    pub token_id: TokenId,
+    /// token amount
+    #[serde(rename = "amount")]
+    pub amount: TokenAmountJsonEip12,
+}
+
+impl From<Token> for TokenJsonEip12 {
+    fn from(t: Token) -> Self {
+        TokenJsonEip12 {
+            token_id: t.token_id,
+            amount: TokenAmountJsonEip12(*t.amount.as_u64()),
+        }
+    }
+}
+
+/// Token amount with bound checks
+#[serde_with::serde_as]
+#[derive(Serialize, PartialEq, Eq, Hash, Debug, Clone, Copy, PartialOrd, Ord)]
+pub struct TokenAmountJsonEip12(
+    // Encodes as string always
+    #[serde_as(as = "serde_with::DisplayFromStr")] u64,
+);
diff --git a/bindings/ergo-lib-c-core/src/lib.rs b/bindings/ergo-lib-c-core/src/lib.rs
index 3f94a55c6..fa57c912d 100644
--- a/bindings/ergo-lib-c-core/src/lib.rs
+++ b/bindings/ergo-lib-c-core/src/lib.rs
@@ -10,39 +10,28 @@
 // #![deny(missing_docs)]
 #![allow(clippy::missing_safety_doc)]
 
+pub mod address;
+pub mod block_header;
+pub mod box_builder;
+pub mod box_selector;
+pub mod collections;
+pub mod constant;
+pub mod context_extension;
+pub mod contract;
+pub mod data_input;
+pub mod ergo_box;
+pub mod ergo_state_ctx;
+pub mod ergo_tree;
+pub mod error_conversion;
+pub mod header;
+pub mod input;
+mod json;
+pub mod reduced;
+pub mod secret_key;
+pub mod token;
+pub mod transaction;
+pub mod tx_builder;
+pub mod util;
+pub mod wallet;
+pub use crate::error::*;
 mod error;
-pub use error::*;
-
-use ergo_lib::ergotree_ir::chain::address::{AddressEncoder, NetworkPrefix};
-
-pub struct Address(ergo_lib::ergotree_ir::chain::address::Address);
-pub type AddressPtr = *mut Address;
-
-pub unsafe fn address_from_testnet(
-    address_str: &str,
-    address_out: *mut AddressPtr,
-) -> Result<(), Error> {
-    let address_out: &mut AddressPtr = if let Some(address_out) = address_out.as_mut() {
-        address_out
-    } else {
-        return Err(Error::InvalidArgument("address_out"));
-    };
-
-    let encoder = AddressEncoder::new(NetworkPrefix::Testnet);
-    let result = encoder.parse_address_from_str(address_str);
-
-    match result {
-        Ok(address) => {
-            *address_out = Box::into_raw(Box::new(Address(address)));
-            Ok(())
-        }
-        Err(err) => Err(Error::misc(err)),
-    }
-}
-
-pub fn address_delete(address: AddressPtr) {
-    if !address.is_null() {
-        let boxed = unsafe { Box::from_raw(address) };
-        std::mem::drop(boxed);
-    }
-}
diff --git a/bindings/ergo-lib-c-core/src/reduced.rs b/bindings/ergo-lib-c-core/src/reduced.rs
new file mode 100644
index 000000000..21a5a3991
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/reduced.rs
@@ -0,0 +1,72 @@
+//! Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input
+//! is augmented with ReducedInput which contains a script reduction result.
+
+use ergo_lib::chain::transaction::{reduced::reduce_tx, TxIoVec};
+
+use crate::{
+    collections::ConstCollectionPtr,
+    ergo_box::ErgoBox,
+    ergo_state_ctx::ConstErgoStateContextPtr,
+    transaction::{ConstUnsignedTransactionPtr, UnsignedTransaction, UnsignedTransactionPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input
+/// is augmented with ReducedInput which contains a script reduction result.
+/// After an unsigned transaction is reduced it can be signed without context.
+/// Thus, it can be serialized and transferred for example to Cold Wallet and signed
+/// in an environment where secrets are known.
+/// see EIP-19 for more details -
+/// <https://github.com/ergoplatform/eips/blob/f280890a4163f2f2e988a0091c078e36912fc531/eip-0019.md>
+#[derive(PartialEq, Debug, Clone)]
+pub struct ReducedTransaction(pub(crate) ergo_lib::chain::transaction::reduced::ReducedTransaction);
+pub type ReducedTransactionPtr = *mut ReducedTransaction;
+pub type ConstReducedTransactionPtr = *const ReducedTransaction;
+
+/// Returns `reduced` transaction, i.e. unsigned transaction where each unsigned input
+/// is augmented with ReducedInput which contains a script reduction result.
+pub unsafe fn reduced_tx_from_unsigned_tx(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    state_context_ptr: ConstErgoStateContextPtr,
+    reduced_tx_out: *mut ReducedTransactionPtr,
+) -> Result<(), Error> {
+    let data_boxes = const_ptr_as_ref(data_boxes_ptr, "data_boxes_ptr")?;
+    let state_context = const_ptr_as_ref(state_context_ptr, "state_context_ptr")?;
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let boxes_to_spend = const_ptr_as_ref(boxes_to_spend_ptr, "boxes_to_spend_ptr")?;
+    let reduced_tx_out = mut_ptr_as_mut(reduced_tx_out, "reduced_tx_out")?;
+    let boxes_to_spend =
+        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect())?;
+    let data_boxes = {
+        let d: Vec<_> = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
+        if d.is_empty() {
+            None
+        } else {
+            Some(TxIoVec::from_vec(d)?)
+        }
+    };
+    let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+        unsigned_tx.0.clone(),
+        boxes_to_spend,
+        data_boxes,
+    )?;
+    let reduced_tx = reduce_tx(tx_context, &state_context.0).map(ReducedTransaction)?;
+    *reduced_tx_out = Box::into_raw(Box::new(reduced_tx));
+    Ok(())
+}
+
+/// Returns the unsigned transation
+pub unsafe fn reduced_tx_unsigned_tx(
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    unsigned_tx_out: *mut UnsignedTransactionPtr,
+) -> Result<(), Error> {
+    let reduced_tx = const_ptr_as_ref(reduced_tx_ptr, "reduced_tx_ptr")?;
+    let unsigned_tx_out = mut_ptr_as_mut(unsigned_tx_out, "unsigned_tx_out")?;
+    *unsigned_tx_out = Box::into_raw(Box::new(UnsignedTransaction(
+        reduced_tx.0.unsigned_tx.clone(),
+    )));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/secret_key.rs b/bindings/ergo-lib-c-core/src/secret_key.rs
new file mode 100644
index 000000000..1fa76a4ff
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/secret_key.rs
@@ -0,0 +1,65 @@
+//! Secret key
+use ergo_lib::ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
+use ergo_lib::wallet;
+use std::convert::TryInto;
+
+use crate::address::{Address, AddressPtr};
+use crate::util::{const_ptr_as_ref, mut_ptr_as_mut};
+use crate::Error;
+
+/// Secret key for the prover
+#[derive(PartialEq, Debug, Clone)]
+pub struct SecretKey(pub(crate) wallet::secret_key::SecretKey);
+pub type SecretKeyPtr = *mut SecretKey;
+pub type ConstSecretKeyPtr = *const SecretKey;
+
+/// Parse dlog secret key from bytes (SEC-1-encoded scalar)
+pub unsafe fn secret_key_from_bytes(
+    bytes_ptr: *const u8,
+    secret_key_out: *mut SecretKeyPtr,
+) -> Result<(), Error> {
+    if bytes_ptr.is_null() {
+        return Err(Error::Misc("bytes_ptr is null".into()));
+    }
+    let bytes = std::slice::from_raw_parts(bytes_ptr, DlogProverInput::SIZE_BYTES);
+    let sized_bytes: &[u8; DlogProverInput::SIZE_BYTES] = bytes.try_into()?;
+    if let Some(k) = wallet::secret_key::SecretKey::dlog_from_bytes(sized_bytes).map(SecretKey) {
+        *secret_key_out = Box::into_raw(Box::new(k));
+        Ok(())
+    } else {
+        Err(Error::Misc("failed to parse scalar".into()))
+    }
+}
+
+/// Generate random key
+pub unsafe fn secret_key_generate_random(secret_key_out: *mut SecretKeyPtr) -> Result<(), Error> {
+    *secret_key_out = Box::into_raw(Box::new(SecretKey(
+        wallet::secret_key::SecretKey::random_dlog(),
+    )));
+    Ok(())
+}
+
+/// Address (encoded public image)
+pub unsafe fn secret_key_get_address(
+    secret_key_ptr: ConstSecretKeyPtr,
+    address_out: *mut AddressPtr,
+) -> Result<(), Error> {
+    let secret_key = const_ptr_as_ref(secret_key_ptr, "secret_key_ptr")?;
+    let address_out = mut_ptr_as_mut(address_out, "address_out")?;
+    *address_out = Box::into_raw(Box::new(Address(
+        secret_key.0.get_address_from_public_image(),
+    )));
+    Ok(())
+}
+
+/// Convert to serialized bytes. Key assumption: 32 bytes have been allocated at the address
+/// pointed-to by `output`.
+pub unsafe fn secret_key_to_bytes(
+    secret_key_ptr: ConstSecretKeyPtr,
+    output: *mut u8,
+) -> Result<(), Error> {
+    let secret_key = const_ptr_as_ref(secret_key_ptr, "secret_key_ptr")?;
+    let src = secret_key.0.to_bytes();
+    std::ptr::copy_nonoverlapping(src.as_ptr(), output, DlogProverInput::SIZE_BYTES);
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/token.rs b/bindings/ergo-lib-c-core/src/token.rs
new file mode 100644
index 000000000..410ecdba7
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/token.rs
@@ -0,0 +1,187 @@
+//! Token types
+
+use std::convert::TryFrom;
+
+use bounded_vec::BoundedVec;
+use ergo_lib::ergotree_ir::chain::{self, base16_bytes::Base16DecodedBytes, digest32::Digest32};
+
+use crate::{
+    ergo_box::ConstBoxIdPtr,
+    json::TokenJsonEip12,
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Token id (32 byte digest)
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct TokenId(pub(crate) chain::token::TokenId);
+pub type TokenIdPtr = *mut TokenId;
+pub type ConstTokenIdPtr = *const TokenId;
+
+/// Create token id from ergo box id (32 byte digest)
+pub unsafe fn token_id_from_box_id(
+    box_id_ptr: ConstBoxIdPtr,
+    token_id_out: *mut TokenIdPtr,
+) -> Result<(), Error> {
+    let box_id = const_ptr_as_ref(box_id_ptr, "box_id_ptr")?;
+    let token_id_out = mut_ptr_as_mut(token_id_out, "token_id_out")?;
+    *token_id_out = Box::into_raw(Box::new(TokenId(chain::token::TokenId::from(
+        box_id.0.clone(),
+    ))));
+    Ok(())
+}
+
+/// Parse token id (32 byte digest) from base16-encoded string
+pub unsafe fn token_id_from_str(str: &str, token_id_out: *mut TokenIdPtr) -> Result<(), Error> {
+    let token_id_out = mut_ptr_as_mut(token_id_out, "token_id_out")?;
+    let base_16_decoded_bytes = Base16DecodedBytes::try_from(str.to_string())?;
+
+    let token_id = Digest32::try_from(base_16_decoded_bytes)
+        .map(|dig| dig.into())
+        .map(TokenId)?;
+    *token_id_out = Box::into_raw(Box::new(token_id));
+    Ok(())
+}
+
+/// Base16 encoded string
+pub unsafe fn token_id_to_str(token_id_ptr: ConstTokenIdPtr) -> Result<String, Error> {
+    let token_id = const_ptr_as_ref(token_id_ptr, "token_id_ptr")?;
+    Ok(token_id.0.clone().into())
+}
+
+/// Token amount with bound checks
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct TokenAmount(pub(crate) chain::token::TokenAmount);
+pub type TokenAmountPtr = *mut TokenAmount;
+pub type ConstTokenAmountPtr = *const TokenAmount;
+
+/// Create from i64 with bounds check
+pub unsafe fn token_amount_from_i64(
+    amount: i64,
+    token_amount_out: *mut TokenAmountPtr,
+) -> Result<(), Error> {
+    let token_amount_out = mut_ptr_as_mut(token_amount_out, "token_amount_out")?;
+    let inner = chain::token::TokenAmount::try_from(amount as u64)?;
+    *token_amount_out = Box::into_raw(Box::new(TokenAmount(inner)));
+    Ok(())
+}
+
+/// Get value as signed 64-bit long
+pub unsafe fn token_amount_as_i64(token_amount_ptr: ConstTokenAmountPtr) -> Result<i64, Error> {
+    let token_amount = const_ptr_as_ref(token_amount_ptr, "token_amount_ptr")?;
+    Ok(i64::from(token_amount.0))
+}
+
+/// Token represented with token id paired with its amount
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct Token(pub(crate) chain::token::Token);
+pub type TokenPtr = *mut Token;
+pub type ConstTokenPtr = *const Token;
+
+/// Create a token with given token id and amount
+pub unsafe fn token_new(
+    token_id_ptr: ConstTokenIdPtr,
+    token_amount_ptr: ConstTokenAmountPtr,
+    token_out: *mut TokenPtr,
+) -> Result<(), Error> {
+    let token_id = const_ptr_as_ref(token_id_ptr, "token_id_ptr")?;
+    let token_amount = const_ptr_as_ref(token_amount_ptr, "token_amount_ptr")?;
+    let token_out = mut_ptr_as_mut(token_out, "token_out")?;
+    *token_out = Box::into_raw(Box::new(Token(chain::token::Token {
+        token_id: token_id.0.clone(),
+        amount: token_amount.0,
+    })));
+    Ok(())
+}
+
+/// Get token id
+pub unsafe fn token_get_id(
+    token_ptr: ConstTokenPtr,
+    token_id_out: *mut TokenIdPtr,
+) -> Result<(), Error> {
+    let token = const_ptr_as_ref(token_ptr, "token_ptr")?;
+    let token_id_out = mut_ptr_as_mut(token_id_out, "token_id_out")?;
+    *token_id_out = Box::into_raw(Box::new(TokenId(token.0.token_id.clone())));
+    Ok(())
+}
+
+/// Get token amount
+pub unsafe fn token_get_amount(
+    token_ptr: ConstTokenPtr,
+    token_amount_out: *mut TokenAmountPtr,
+) -> Result<(), Error> {
+    let token = const_ptr_as_ref(token_ptr, "token_ptr")?;
+    let token_amount_out = mut_ptr_as_mut(token_amount_out, "token_amount_out")?;
+    *token_amount_out = Box::into_raw(Box::new(TokenAmount(token.0.amount)));
+    Ok(())
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+pub unsafe fn token_to_json_eip12(token_ptr: ConstTokenPtr) -> Result<String, Error> {
+    let token = const_ptr_as_ref(token_ptr, "token_ptr")?;
+    let t_dapp: TokenJsonEip12 = token.0.clone().into();
+    let s = serde_json::to_string(&t_dapp)?;
+    Ok(s)
+}
+
+/// A Bounded Vector for Tokens. A Box can have between 1 and 255 tokens
+pub type BoxTokens = BoundedVec<Token, 1, 255>;
+
+/// Array of tokens. Note that we're not using `crate::collections::Collection` here due to the
+/// use of the `BoundedVec`.
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct Tokens(pub(crate) Option<BoxTokens>);
+pub type TokensPtr = *mut Tokens;
+pub type ConstTokensPtr = *const Tokens;
+
+/// Create an empty collection
+pub unsafe fn tokens_new(tokens_out: *mut TokensPtr) -> Result<(), Error> {
+    let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
+    *tokens_out = Box::into_raw(Box::new(Tokens(None)));
+    Ok(())
+}
+
+/// Returns length of collection
+pub unsafe fn tokens_len(tokens_ptr: ConstTokensPtr) -> Result<usize, Error> {
+    let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
+    Ok(tokens.0.as_ref().map(BoxTokens::len).unwrap_or(0))
+}
+
+/// If token at given index exists, allocate a copy and store in `token_out` and return `Ok(true)`.
+/// If token doesn't exist at the given index return Ok(false).
+pub unsafe fn tokens_get(
+    tokens_ptr: ConstTokensPtr,
+    index: usize,
+    token_out: *mut TokenPtr,
+) -> Result<bool, Error> {
+    let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
+    let token_out = mut_ptr_as_mut(token_out, "token_out")?;
+    if let Some(tokens) = tokens.0.as_ref() {
+        if let Some(token) = tokens.get(index) {
+            *token_out = Box::into_raw(Box::new(token.clone()));
+            return Ok(true);
+        }
+    }
+    Ok(false)
+}
+
+/// Add token to the end of the collection. There is a maximum capacity of 255 token, and adding
+/// more returns an error.
+pub unsafe fn tokens_add(tokens_ptr: TokensPtr, token_ptr: ConstTokenPtr) -> Result<(), Error> {
+    let tokens = mut_ptr_as_mut(tokens_ptr, "tokens_ptr")?;
+    let token = const_ptr_as_ref(token_ptr, "token_ptr")?;
+    if tokens.0.is_some() {
+        let mut new_vec = tokens.0.as_ref().unwrap().as_vec().clone();
+        if new_vec.len() == 255 {
+            return Err(Error::Misc(
+                "Tokens.add: cannot have more than 255 tokens".into(),
+            ));
+        } else {
+            new_vec.push(token.clone());
+            tokens.0 = Some(BoxTokens::from_vec(new_vec).unwrap());
+        }
+    } else {
+        tokens.0 = Some(BoxTokens::from([token.clone()]));
+    }
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/transaction.rs b/bindings/ergo-lib-c-core/src/transaction.rs
new file mode 100644
index 000000000..c07908873
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/transaction.rs
@@ -0,0 +1,290 @@
+//! Ergo transaction
+
+use std::convert::{TryFrom, TryInto};
+
+use ergo_lib::{
+    chain,
+    ergotree_ir::chain::base16_bytes::{Base16DecodedBytes, Base16EncodedBytes},
+};
+
+use crate::{
+    collections::{Collection, CollectionPtr, ConstCollectionPtr},
+    data_input::DataInput,
+    ergo_box::{ErgoBox, ErgoBoxCandidate},
+    input::{Input, UnsignedInput},
+    json::{TransactionJsonEip12, UnsignedTransactionJsonEip12},
+    util::{const_ptr_as_ref, mut_ptr_as_mut, ByteArray},
+    Error,
+};
+
+/// Unsigned (inputs without proofs) transaction
+#[derive(PartialEq, Debug, Clone)]
+pub struct UnsignedTransaction(pub(crate) chain::transaction::unsigned::UnsignedTransaction);
+pub type UnsignedTransactionPtr = *mut UnsignedTransaction;
+pub type ConstUnsignedTransactionPtr = *const UnsignedTransaction;
+
+/// Get id for transaction
+pub unsafe fn unsigned_tx_id(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    tx_id_out: *mut TxIdPtr,
+) -> Result<(), Error> {
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let tx_id_out = mut_ptr_as_mut(tx_id_out, "tx_id_out")?;
+    *tx_id_out = Box::into_raw(Box::new(TxId(unsigned_tx.0.id())));
+    Ok(())
+}
+
+/// Inputs for transaction
+pub unsafe fn unsigned_tx_inputs(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    unsigned_inputs_out: *mut CollectionPtr<UnsignedInput>,
+) -> Result<(), Error> {
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let unsigned_inputs_out = mut_ptr_as_mut(unsigned_inputs_out, "unsigned_inputs_out")?;
+    *unsigned_inputs_out = Box::into_raw(Box::new(Collection(
+        unsigned_tx
+            .0
+            .inputs
+            .as_vec()
+            .clone()
+            .into_iter()
+            .map(UnsignedInput)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Data inputs for transaction
+pub unsafe fn unsigned_tx_data_inputs(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    data_inputs_out: *mut CollectionPtr<DataInput>,
+) -> Result<(), Error> {
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let data_inputs_out = mut_ptr_as_mut(data_inputs_out, "data_inputs_out")?;
+    *data_inputs_out = Box::into_raw(Box::new(Collection(
+        unsigned_tx
+            .0
+            .data_inputs
+            .as_ref()
+            .map(|v| v.as_vec().clone())
+            .unwrap_or_else(Vec::new)
+            .into_iter()
+            .map(DataInput)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Output candidates for transaction
+pub unsafe fn unsigned_tx_output_candidates(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    ergo_box_candidates_out: *mut CollectionPtr<ErgoBoxCandidate>,
+) -> Result<(), Error> {
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let ergo_box_candidates_out =
+        mut_ptr_as_mut(ergo_box_candidates_out, "ergo_box_candidates_out")?;
+    *ergo_box_candidates_out = Box::into_raw(Box::new(Collection(
+        unsigned_tx
+            .0
+            .output_candidates
+            .iter()
+            .map(|ebc| ErgoBoxCandidate(ebc.clone()))
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Parse from JSON. Supports Ergo Node/Explorer API and box values and token amount encoded as
+/// strings
+pub unsafe fn unsigned_tx_from_json(
+    json: &str,
+    unsigned_tx_out: *mut UnsignedTransactionPtr,
+) -> Result<(), Error> {
+    let unsigned_tx_out = mut_ptr_as_mut(unsigned_tx_out, "unsigned_tx_out")?;
+    let unsigned_tx = serde_json::from_str(json).map(UnsignedTransaction)?;
+    *unsigned_tx_out = Box::into_raw(Box::new(unsigned_tx));
+    Ok(())
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+pub unsafe fn unsigned_tx_to_json(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+) -> Result<String, Error> {
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let s = serde_json::to_string(&unsigned_tx.0)?;
+    Ok(s)
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+pub unsafe fn unsigned_tx_to_json_eip12(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+) -> Result<String, Error> {
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let tx_dapp: UnsignedTransactionJsonEip12 = unsigned_tx.0.clone().into();
+    let s = serde_json::to_string(&tx_dapp)?;
+    Ok(s)
+}
+
+/// Transaction id
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct TxId(pub(crate) chain::transaction::TxId);
+pub type TxIdPtr = *mut TxId;
+pub type ConstTxIdPtr = *const TxId;
+
+/// Convert a hex string into a TxId
+pub unsafe fn tx_id_from_str(str: &str, tx_id_out: *mut TxIdPtr) -> Result<(), Error> {
+    let tx_id_out = mut_ptr_as_mut(tx_id_out, "tx_id_out")?;
+    let bytes = Base16DecodedBytes::try_from(str.to_string())?;
+    let tx_id = bytes
+        .try_into()
+        .map(|digest| TxId(chain::transaction::TxId(digest)))?;
+    *tx_id_out = Box::into_raw(Box::new(tx_id));
+    Ok(())
+}
+
+/// Get the tx id as bytes
+pub unsafe fn tx_id_to_str(tx_id_ptr: ConstTxIdPtr) -> Result<String, Error> {
+    let tx_id = const_ptr_as_ref(tx_id_ptr, "tx_id_ptr")?;
+    let base16_bytes = Base16EncodedBytes::new(tx_id.0 .0 .0.as_ref());
+    Ok(base16_bytes.into())
+}
+
+/**
+ * ErgoTransaction is an atomic state transition operation. It destroys Boxes from the state
+ * and creates new ones. If transaction is spending boxes protected by some non-trivial scripts,
+ * its inputs should also contain proof of spending correctness - context extension (user-defined
+ * key-value map) and data inputs (links to existing boxes in the state) that may be used during
+ * script reduction to crypto, signatures that satisfies the remaining cryptographic protection
+ * of the script.
+ * Transactions are not encrypted, so it is possible to browse and view every transaction ever
+ * collected into a block.
+ */
+pub struct Transaction(pub(crate) chain::transaction::Transaction);
+pub type TransactionPtr = *mut Transaction;
+pub type ConstTransactionPtr = *const 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 unsafe fn tx_from_unsigned_tx(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    proofs_ptr: ConstCollectionPtr<ByteArray>,
+    tx_out: *mut TransactionPtr,
+) -> Result<(), Error> {
+    let proofs = const_ptr_as_ref(proofs_ptr, "proofs_ptr")?;
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let tx_out = mut_ptr_as_mut(tx_out, "tx_out")?;
+    let tx = chain::transaction::Transaction::from_unsigned_tx(
+        unsigned_tx.0.clone(),
+        proofs
+            .0
+            .iter()
+            .cloned()
+            .map(|bytes| bytes.0.into())
+            .collect(),
+    )
+    .map(Transaction)?;
+    *tx_out = Box::into_raw(Box::new(tx));
+    Ok(())
+}
+
+/// Get id for transaction
+pub unsafe fn tx_id(tx_ptr: ConstTransactionPtr, tx_id_out: *mut TxIdPtr) -> Result<(), Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let tx_id_out = mut_ptr_as_mut(tx_id_out, "tx_id_out")?;
+    *tx_id_out = Box::into_raw(Box::new(TxId(tx.0.id())));
+    Ok(())
+}
+
+/// Parse from JSON. Supports Ergo Node/Explorer API and box values and token amount encoded as
+/// strings
+pub unsafe fn tx_from_json(json: &str, tx_out: *mut TransactionPtr) -> Result<(), Error> {
+    let tx_out = mut_ptr_as_mut(tx_out, "tx_out")?;
+    let tx = serde_json::from_str(json).map(Transaction)?;
+    *tx_out = Box::into_raw(Box::new(tx));
+    Ok(())
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+pub unsafe fn tx_to_json(tx_ptr: ConstTransactionPtr) -> Result<String, Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let s = serde_json::to_string(&tx.0)?;
+    Ok(s)
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+pub unsafe fn tx_to_json_eip12(tx_ptr: ConstTransactionPtr) -> Result<String, Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let tx_dapp: TransactionJsonEip12 = tx.0.clone().into();
+    let s = serde_json::to_string(&tx_dapp)?;
+    Ok(s)
+}
+
+/// Inputs for transaction
+pub unsafe fn tx_inputs(
+    tx_ptr: ConstTransactionPtr,
+    inputs_out: *mut CollectionPtr<Input>,
+) -> Result<(), Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let inputs_out = mut_ptr_as_mut(inputs_out, "inputs_out")?;
+    *inputs_out = Box::into_raw(Box::new(Collection(
+        tx.0.inputs
+            .as_vec()
+            .clone()
+            .into_iter()
+            .map(Input)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Data inputs for transaction
+pub unsafe fn tx_data_inputs(
+    tx_ptr: ConstTransactionPtr,
+    data_inputs_out: *mut CollectionPtr<DataInput>,
+) -> Result<(), Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let data_inputs_out = mut_ptr_as_mut(data_inputs_out, "data_inputs_out")?;
+    *data_inputs_out = Box::into_raw(Box::new(Collection(
+        tx.0.data_inputs
+            .as_ref()
+            .map(|v| v.as_vec().clone())
+            .unwrap_or_else(Vec::new)
+            .into_iter()
+            .map(DataInput)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Output candidates for transaction
+pub unsafe fn tx_output_candidates(
+    tx_ptr: ConstTransactionPtr,
+    ergo_box_candidates_out: *mut CollectionPtr<ErgoBoxCandidate>,
+) -> Result<(), Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let ergo_box_candidates_out =
+        mut_ptr_as_mut(ergo_box_candidates_out, "ergo_box_candidates_out")?;
+    *ergo_box_candidates_out = Box::into_raw(Box::new(Collection(
+        tx.0.output_candidates
+            .iter()
+            .map(|ebc| ErgoBoxCandidate(ebc.clone()))
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Returns ErgoBox's created from ErgoBoxCandidate's with tx id and indices
+pub unsafe fn tx_outputs(
+    tx_ptr: ConstTransactionPtr,
+    ergo_box_out: *mut CollectionPtr<ErgoBox>,
+) -> Result<(), Error> {
+    let tx = const_ptr_as_ref(tx_ptr, "tx_ptr")?;
+    let ergo_box_out = mut_ptr_as_mut(ergo_box_out, "ergo_box_candidates_out")?;
+    *ergo_box_out = Box::into_raw(Box::new(Collection(
+        tx.0.outputs
+            .iter()
+            .map(|ebc| ErgoBox(ebc.clone()))
+            .collect(),
+    )));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/tx_builder.rs b/bindings/ergo-lib-c-core/src/tx_builder.rs
new file mode 100644
index 000000000..a6561c8a5
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/tx_builder.rs
@@ -0,0 +1,180 @@
+//! Unsigned transaction builder
+use ergo_lib::wallet;
+
+use crate::{
+    address::{Address, AddressPtr, ConstAddressPtr},
+    box_selector::{BoxSelection, BoxSelectionPtr, ConstBoxSelectionPtr},
+    collections::{Collection, CollectionPtr, ConstCollectionPtr},
+    data_input::DataInput,
+    ergo_box::{BoxValue, BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidate},
+    transaction::{UnsignedTransaction, UnsignedTransactionPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// Unsigned transaction builder
+pub struct TxBuilder(
+    wallet::tx_builder::TxBuilder<ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox>,
+);
+pub type TxBuilderPtr = *mut TxBuilder;
+pub type ConstTxBuilderPtr = *const TxBuilder;
+
+/// Suggested transaction fee (semi-default value used across wallets and dApps as of Oct 2020)
+pub unsafe fn tx_builder_suggested_tx_fee(value_out: *mut BoxValuePtr) -> Result<(), Error> {
+    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
+    *value_out = Box::into_raw(Box::new(BoxValue(wallet::tx_builder::SUGGESTED_TX_FEE())));
+    Ok(())
+}
+
+/// Creates new TxBuilder
+/// `box_selection` - selected input boxes (via [`super::box_selector`])
+/// `output_candidates` - output boxes to be "created" in this transaction,
+/// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
+/// `fee_amount` - miner's fee,
+/// `change_address` - change (inputs - outputs) will be sent to this address,
+/// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
+/// will be given to miners,
+pub unsafe fn tx_builder_new(
+    box_selection_ptr: ConstBoxSelectionPtr,
+    output_candidates_ptr: ConstCollectionPtr<ErgoBoxCandidate>,
+    current_height: u32,
+    fee_amount_ptr: ConstBoxValuePtr,
+    change_address_ptr: ConstAddressPtr,
+    min_change_value_ptr: ConstBoxValuePtr,
+    tx_builder_out: *mut TxBuilderPtr,
+) -> Result<(), Error> {
+    let box_selection = const_ptr_as_ref(box_selection_ptr, "box_selection_ptr")?;
+    let output_candidates = const_ptr_as_ref(output_candidates_ptr, "output_candidates_ptr")?;
+    let fee_amount = const_ptr_as_ref(fee_amount_ptr, "fee_amount_ptr")?;
+    let change_address = const_ptr_as_ref(change_address_ptr, "change_address_ptr")?;
+    let min_change_value = const_ptr_as_ref(min_change_value_ptr, "min_change_value_ptr")?;
+    let tx_builder_out = mut_ptr_as_mut(tx_builder_out, "tx_builder_out")?;
+    *tx_builder_out = Box::into_raw(Box::new(TxBuilder(
+        ergo_lib::wallet::tx_builder::TxBuilder::new(
+            box_selection.0.clone(),
+            output_candidates
+                .0
+                .clone()
+                .into_iter()
+                .map(|b| b.0)
+                .collect(),
+            current_height,
+            fee_amount.0,
+            change_address.0.clone(),
+            min_change_value.0,
+        ),
+    )));
+    Ok(())
+}
+
+/// Set transaction's data inputs
+pub unsafe fn tx_builder_set_data_inputs(
+    tx_builder_mut: TxBuilderPtr,
+    data_inputs_ptr: ConstCollectionPtr<DataInput>,
+) -> Result<(), Error> {
+    let data_inputs = const_ptr_as_ref(data_inputs_ptr, "data_inputs_ptr")?;
+    let tx_builder_mut = mut_ptr_as_mut(tx_builder_mut, "tx_builder_mut")?;
+    tx_builder_mut
+        .0
+        .set_data_inputs(data_inputs.0.clone().into_iter().map(|d| d.0).collect());
+    Ok(())
+}
+
+/// Build the unsigned transaction
+pub unsafe fn tx_builder_build(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    unsigned_transaction_out: *mut UnsignedTransactionPtr,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let unsigned_transaction_out =
+        mut_ptr_as_mut(unsigned_transaction_out, "unsigned_transaction_out")?;
+    let unsigned_tx = tx_builder.0.clone().build().map(UnsignedTransaction)?;
+    *unsigned_transaction_out = Box::into_raw(Box::new(unsigned_tx));
+    Ok(())
+}
+
+/// Get box selection
+pub unsafe fn tx_builder_box_selection(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    box_selection_out: *mut BoxSelectionPtr,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let box_selection_out = mut_ptr_as_mut(box_selection_out, "box_selection_out")?;
+    *box_selection_out = Box::into_raw(Box::new(BoxSelection(tx_builder.0.box_selection())));
+    Ok(())
+}
+
+/// Get data inputs
+pub unsafe fn tx_builder_data_inputs(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    data_inputs_out: *mut CollectionPtr<DataInput>,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let data_inputs_out = mut_ptr_as_mut(data_inputs_out, "data_inputs_out")?;
+    *data_inputs_out = Box::into_raw(Box::new(Collection(
+        tx_builder
+            .0
+            .data_inputs()
+            .into_iter()
+            .map(DataInput)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Get outputs EXCLUDING fee and change
+pub unsafe fn tx_builder_output_candidates(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    output_candidates_out: *mut CollectionPtr<ErgoBoxCandidate>,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let output_candidates_out = mut_ptr_as_mut(output_candidates_out, "output_candidates_out")?;
+    *output_candidates_out = Box::into_raw(Box::new(Collection(
+        tx_builder
+            .0
+            .output_candidates()
+            .into_iter()
+            .map(ErgoBoxCandidate)
+            .collect(),
+    )));
+    Ok(())
+}
+
+/// Get current height
+pub unsafe fn tx_builder_current_height(tx_builder_ptr: ConstTxBuilderPtr) -> Result<u32, Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    Ok(tx_builder.0.current_height())
+}
+
+/// Get fee amount
+pub unsafe fn tx_builder_fee_amount(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let value_out = mut_ptr_as_mut(value_out, "value_out")?;
+    *value_out = Box::into_raw(Box::new(BoxValue(tx_builder.0.fee_amount())));
+    Ok(())
+}
+
+/// Get change address
+pub unsafe fn tx_builder_change_address(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    address_out: *mut AddressPtr,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let address_out = mut_ptr_as_mut(address_out, "address_out")?;
+    *address_out = Box::into_raw(Box::new(Address(tx_builder.0.change_address())));
+    Ok(())
+}
+
+/// Get min change value
+pub unsafe fn tx_builder_min_change_value(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    min_change_value_out: *mut BoxValuePtr,
+) -> Result<(), Error> {
+    let tx_builder = const_ptr_as_ref(tx_builder_ptr, "tx_builder_ptr")?;
+    let min_change_value_out = mut_ptr_as_mut(min_change_value_out, "min_change_value_out")?;
+    *min_change_value_out = Box::into_raw(Box::new(BoxValue(tx_builder.0.min_change_value())));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/util.rs b/bindings/ergo-lib-c-core/src/util.rs
new file mode 100644
index 000000000..e6891295f
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/util.rs
@@ -0,0 +1,51 @@
+//! Utility functions and types
+
+use crate::error::Error;
+
+/// Try to cast const* pointer to immutable reference.
+pub(crate) unsafe fn const_ptr_as_ref<'a, T>(
+    ptr: *const T,
+    ptr_name: &'static str,
+) -> Result<&'a T, Error> {
+    if let Some(r) = ptr.as_ref() {
+        Ok(r)
+    } else {
+        Err(Error::InvalidArgument(ptr_name))
+    }
+}
+
+/// Try to cast mut* pointer to mutable reference.
+pub(crate) unsafe fn mut_ptr_as_mut<'a, T>(
+    ptr: *mut T,
+    ptr_name: &'static str,
+) -> Result<&'a mut T, Error> {
+    if let Some(r) = ptr.as_mut() {
+        Ok(r)
+    } else {
+        Err(Error::InvalidArgument(ptr_name))
+    }
+}
+
+/// Simple wrapper around a `Vec<u8>`.
+#[derive(Clone)]
+pub struct ByteArray(pub Vec<u8>);
+pub type ByteArrayPtr = *mut ByteArray;
+pub type ConstByteArrayPtr = *const ByteArray;
+
+pub unsafe fn byte_array_from_raw_parts(
+    ptr: *const u8,
+    len: usize,
+    byte_array_out: *mut ByteArrayPtr,
+) -> Result<(), Error> {
+    let slice = std::slice::from_raw_parts(ptr, len);
+    let byte_array_out = mut_ptr_as_mut(byte_array_out, "byte_array_out")?;
+    *byte_array_out = Box::into_raw(Box::new(ByteArray(Vec::from(slice))));
+    Ok(())
+}
+
+/// Returns true iff the dereferenced pointer values are equal.
+pub unsafe fn deref_eq<T: Eq>(x_ptr: *const T, y_ptr: *const T) -> bool {
+    let x = x_ptr.as_ref().unwrap();
+    let y = y_ptr.as_ref().unwrap();
+    x == y
+}
diff --git a/bindings/ergo-lib-c-core/src/wallet.rs b/bindings/ergo-lib-c-core/src/wallet.rs
new file mode 100644
index 000000000..67ff21c8c
--- /dev/null
+++ b/bindings/ergo-lib-c-core/src/wallet.rs
@@ -0,0 +1,104 @@
+//! Wallet-like features
+
+use ergo_lib::chain::transaction::TxIoVec;
+
+use crate::{
+    collections::ConstCollectionPtr,
+    ergo_box::ErgoBox,
+    ergo_state_ctx::ConstErgoStateContextPtr,
+    reduced::ConstReducedTransactionPtr,
+    secret_key::SecretKey,
+    transaction::{ConstUnsignedTransactionPtr, Transaction, TransactionPtr},
+    util::{const_ptr_as_ref, mut_ptr_as_mut},
+    Error,
+};
+
+/// A collection of secret keys. This simplified signing by matching the secret keys to the correct inputs automatically.
+pub struct Wallet(ergo_lib::wallet::Wallet);
+pub type WalletPtr = *mut Wallet;
+pub type ConstWalletPtr = *const Wallet;
+
+/// Create `Wallet` instance loading secret key from mnemonic Returns Err if a DlogSecretKey cannot be
+/// parsed from the provided phrase
+pub unsafe fn wallet_from_mnemonic(
+    mnemonic_phrase: &str,
+    mnemonic_pass: &str,
+    wallet_out: *mut WalletPtr,
+) -> Result<(), Error> {
+    let wallet_out = mut_ptr_as_mut(wallet_out, "wallet_out")?;
+    if let Some(wallet_inner) =
+        ergo_lib::wallet::Wallet::from_mnemonic(mnemonic_phrase, mnemonic_pass)
+    {
+        *wallet_out = Box::into_raw(Box::new(Wallet(wallet_inner)));
+        Ok(())
+    } else {
+        Err(Error::Misc(
+            "Wallet.from_mnemonic: DlogSecretKey can't be parsed from the provided phrase".into(),
+        ))
+    }
+}
+
+/// Create `Wallet` from secrets
+pub unsafe fn wallet_from_secrets(
+    secret_keys_ptr: ConstCollectionPtr<SecretKey>,
+    wallet_out: *mut WalletPtr,
+) -> Result<(), Error> {
+    let secret_keys = const_ptr_as_ref(secret_keys_ptr, "secret_keys_ptr")?;
+    let wallet_out = mut_ptr_as_mut(wallet_out, "wallet_out")?;
+    *wallet_out = Box::into_raw(Box::new(Wallet(ergo_lib::wallet::Wallet::from_secrets(
+        secret_keys.0.clone().into_iter().map(|s| s.0).collect(),
+    ))));
+    Ok(())
+}
+
+/// Signs a transaction
+pub unsafe fn wallet_sign_transaction(
+    wallet_ptr: ConstWalletPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    transaction_out: *mut TransactionPtr,
+) -> Result<(), Error> {
+    let wallet = const_ptr_as_ref(wallet_ptr, "wallet_ptr")?;
+    let state_context = const_ptr_as_ref(state_context_ptr, "state_context_ptr")?;
+    let unsigned_tx = const_ptr_as_ref(unsigned_tx_ptr, "unsigned_tx_ptr")?;
+    let boxes_to_spend = const_ptr_as_ref(boxes_to_spend_ptr, "boxes_to_spend_ptr")?;
+    let data_boxes = const_ptr_as_ref(data_boxes_ptr, "data_boxes_ptr")?;
+    let transaction_out = mut_ptr_as_mut(transaction_out, "transaction_out")?;
+    let boxes_to_spend =
+        TxIoVec::from_vec(boxes_to_spend.0.clone().into_iter().map(|b| b.0).collect())?;
+    let data_boxes = {
+        let d: Vec<_> = data_boxes.0.clone().into_iter().map(|b| b.0).collect();
+        if d.is_empty() {
+            None
+        } else {
+            Some(TxIoVec::from_vec(d)?)
+        }
+    };
+    let tx_context = ergo_lib::wallet::signing::TransactionContext::new(
+        unsigned_tx.0.clone(),
+        boxes_to_spend,
+        data_boxes,
+    )?;
+    let tx = wallet.0.sign_transaction(tx_context, &state_context.0)?;
+    *transaction_out = Box::into_raw(Box::new(Transaction(tx)));
+    Ok(())
+}
+
+/// Signs a reduced transaction (generating proofs for inputs)
+pub unsafe fn wallet_sign_reduced_transaction(
+    wallet_ptr: ConstWalletPtr,
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    transaction_out: *mut TransactionPtr,
+) -> Result<(), Error> {
+    let wallet = const_ptr_as_ref(wallet_ptr, "wallet_ptr")?;
+    let reduced_tx = const_ptr_as_ref(reduced_tx_ptr, "reduced_tx_ptr")?;
+    let transaction_out = mut_ptr_as_mut(transaction_out, "transaction_out")?;
+    let tx = wallet
+        .0
+        .sign_reduced_transaction(reduced_tx.0.clone())
+        .map(Transaction)?;
+    *transaction_out = Box::into_raw(Box::new(tx));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c/Cargo.toml b/bindings/ergo-lib-c/Cargo.toml
index b31a04d58..8769939f8 100644
--- a/bindings/ergo-lib-c/Cargo.toml
+++ b/bindings/ergo-lib-c/Cargo.toml
@@ -17,3 +17,4 @@ crate-type = ["staticlib"]
 [dependencies]
 ergo-lib = {version = "^0.14.0", path = "../../ergo-lib"}
 ergo-lib-c-core = {version = "^0.14.0", path = "../ergo-lib-c-core"}
+paste = "^1.0"
\ No newline at end of file
diff --git a/bindings/ergo-lib-c/README.md b/bindings/ergo-lib-c/README.md
index 069615b66..a95c1c8cf 100644
--- a/bindings/ergo-lib-c/README.md
+++ b/bindings/ergo-lib-c/README.md
@@ -2,3 +2,21 @@
 
 A thin wrapper for core Rust part of [C bindings](../ergo-lib-c-core)
 Does not use Rust types in the API and is suitable for C FFI.
+
+## Build instructions
+
+The following command builds the project:
+
+```
+cargo build -p ergo-lib-c
+```
+
+The C header file is generated with [cbindgen](https://github.com/eqrion/cbindgen). Run the following
+command:
+```
+cbindgen --config cbindgen.toml --crate ergo-lib-c --output h/ergo_wallet.h
+```
+**Note that we require a nightly version of rustc just for this command.** This is because we use
+macros to generate some C-types and `cbindgen` cannot directly generate types through them. However
+`cbindgen` gives us the option to expand macros, which can only be done through a nightly version of
+`rustc`. **The crate itself can always be compiled by a stable version of `rustc`.**
\ No newline at end of file
diff --git a/bindings/ergo-lib-c/cbindgen.toml b/bindings/ergo-lib-c/cbindgen.toml
index 1ebdce229..37a289c63 100644
--- a/bindings/ergo-lib-c/cbindgen.toml
+++ b/bindings/ergo-lib-c/cbindgen.toml
@@ -8,7 +8,6 @@
 language = "C"
 
 
-
 ############## Options for Wrapping the Contents of the Header #################
 
 # header = "/* Text to put at the beginning of the generated file. Probably a license. */"
@@ -138,7 +137,7 @@ extra_bindings = []
 
 
 [parse.expand]
-crates = []
+crates = ["ergo-lib-c"]
 all_features = false
 default_features = true
 features = []
diff --git a/bindings/ergo-lib-c/src/address.rs b/bindings/ergo-lib-c/src/address.rs
new file mode 100644
index 000000000..17a85b8da
--- /dev/null
+++ b/bindings/ergo-lib-c/src/address.rs
@@ -0,0 +1,67 @@
+use ergo_lib_c_core::{
+    address::{
+        address_delete, address_from_base58, address_from_mainnet, address_from_testnet,
+        address_to_base58, address_type_prefix, AddressPtr, ConstAddressPtr, NetworkPrefix,
+    },
+    Error,
+};
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+};
+
+use crate::ErrorPtr;
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_address_from_testnet(
+    address_str: *const c_char,
+    address_out: *mut AddressPtr,
+) -> ErrorPtr {
+    let address = CStr::from_ptr(address_str).to_string_lossy();
+    let res = address_from_testnet(&address, address_out);
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_address_from_mainnet(
+    address_str: *const c_char,
+    address_out: *mut AddressPtr,
+) -> ErrorPtr {
+    let address = CStr::from_ptr(address_str).to_string_lossy();
+    let res = address_from_mainnet(&address, address_out);
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_address_from_base58(
+    address_str: *const c_char,
+    address_out: *mut AddressPtr,
+) -> ErrorPtr {
+    let address = CStr::from_ptr(address_str).to_string_lossy();
+    let res = address_from_base58(&address, address_out);
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_address_to_base58(
+    address: ConstAddressPtr,
+    network_prefix: NetworkPrefix,
+    _address_str: *mut *const c_char,
+) {
+    #[allow(clippy::unwrap_used)]
+    {
+        let s = address_to_base58(address, network_prefix).unwrap();
+        *_address_str = CString::new(s).unwrap().into_raw();
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_address_type_prefix(address: ConstAddressPtr) -> u8 {
+    #[allow(clippy::unwrap_used)]
+    (address_type_prefix(address).unwrap() as u8)
+}
+
+#[no_mangle]
+pub extern "C" fn ergo_lib_address_delete(address: AddressPtr) {
+    address_delete(address)
+}
diff --git a/bindings/ergo-lib-c/src/block_header.rs b/bindings/ergo-lib-c/src/block_header.rs
new file mode 100644
index 000000000..c3000bcd1
--- /dev/null
+++ b/bindings/ergo-lib-c/src/block_header.rs
@@ -0,0 +1,30 @@
+//! Block header
+use paste::paste;
+
+use ergo_lib_c_core::{
+    block_header::{block_header_from_json, BlockHeader, BlockHeaderPtr, ConstBlockHeaderPtr},
+    Error,
+};
+use std::{ffi::CStr, os::raw::c_char};
+
+use crate::{delete_ptr, ErrorPtr};
+
+/// Parse BlockHeader array from JSON (Node API)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_block_header_from_json(
+    json_str: *const c_char,
+    block_header_out: *mut BlockHeaderPtr,
+) -> ErrorPtr {
+    let json = CStr::from_ptr(json_str).to_string_lossy();
+    let res = block_header_from_json(&json, block_header_out);
+    Error::c_api_from(res)
+}
+
+/// Delete `BlockHeader`
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_block_header_delete(ptr: BlockHeaderPtr) {
+    delete_ptr(ptr)
+}
+
+make_collection!(BlockHeaders, BlockHeader);
+make_ffi_eq!(BlockHeader);
diff --git a/bindings/ergo-lib-c/src/box_builder.rs b/bindings/ergo-lib-c/src/box_builder.rs
new file mode 100644
index 000000000..87831fb4d
--- /dev/null
+++ b/bindings/ergo-lib-c/src/box_builder.rs
@@ -0,0 +1,190 @@
+//! `ErgoBoxCandidate` builder
+
+use ergo_lib_c_core::{
+    box_builder::*,
+    constant::{ConstConstantPtr, ConstantPtr},
+    contract::ConstContractPtr,
+    ergo_box::{BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidatePtr, NonMandatoryRegisterId},
+    token::{ConstTokenAmountPtr, ConstTokenIdPtr, ConstTokenPtr},
+    Error,
+};
+use std::{ffi::CStr, os::raw::c_char};
+
+use crate::{delete_ptr, ErrorPtr, ReturnNum, ReturnOption};
+
+/// Create builder with required box parameters:
+/// `value` - amount of money associated with the box
+/// `contract` - guarding contract([`Contract`]), which should be evaluated to true in order
+/// to open(spend) this box
+/// `creation_height` - height when a transaction containing the box is created.
+/// It should not exceed height of the block, containing the transaction with this box.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_new(
+    value_ptr: ConstBoxValuePtr,
+    contract_ptr: ConstContractPtr,
+    creation_height: u32,
+    builder_out: *mut ErgoBoxCandidateBuilderPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_new(value_ptr, contract_ptr, creation_height, builder_out).unwrap()
+}
+
+/// Set minimal value (per byte of the serialized box size)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_set_min_box_value_per_byte(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    new_min_value_per_byte: u32,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_set_min_box_value_per_byte(builder_mut, new_min_value_per_byte)
+        .unwrap();
+}
+
+/// Get minimal value (per byte of the serialized box size)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_min_box_value_per_byte(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+) -> u32 {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_min_box_value_per_byte(builder_ptr).unwrap()
+}
+
+/// Set new box value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_set_value(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    value_ptr: ConstBoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_set_value(builder_mut, value_ptr).unwrap();
+}
+
+/// Get box value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_value(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    value_out: *mut BoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_value(builder_ptr, value_out).unwrap();
+}
+
+/// Calculate serialized box size(in bytes)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_calc_box_size_bytes(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+) -> ReturnNum<usize> {
+    match ergo_box_candidate_builder_calc_box_size_bytes(builder_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Calculate minimal box value for the current box serialized size(in bytes)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_calc_min_box_value(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    value_out: *mut BoxValuePtr,
+) -> ErrorPtr {
+    let res = ergo_box_candidate_builder_calc_min_box_value(builder_ptr, value_out);
+    Error::c_api_from(res)
+}
+
+/// Set register with a given id (R4-R9) to the given value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_set_register_value(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    register_id: NonMandatoryRegisterId,
+    constant_ptr: ConstConstantPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_set_register_value(builder_mut, register_id, constant_ptr).unwrap();
+}
+
+/// Returns register value for the given register id (R4-R9), or None if the register is empty
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_register_value(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    register_id: NonMandatoryRegisterId,
+    constant_out: *mut ConstantPtr,
+) -> ReturnOption {
+    match ergo_box_candidate_builder_register_value(builder_ptr, register_id, constant_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Delete register value(make register empty) for the given register id (R4-R9)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_delete_register_value(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    register_id: NonMandatoryRegisterId,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_delete_register_value(builder_mut, register_id).unwrap();
+}
+
+/// Mint token, as defined in <https://github.com/ergoplatform/eips/blob/master/eip-0004.md>
+/// `token` - token id(box id of the first input box in transaction) and token amount,
+/// `token_name` - token name (will be encoded in R4),
+/// `token_desc` - token description (will be encoded in R5),
+/// `num_decimals` - number of decimals (will be encoded in R6)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_mint_token(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    token_ptr: ConstTokenPtr,
+    token_name: *const c_char,
+    token_desc: *const c_char,
+    num_decimals: usize,
+) {
+    let token_name = CStr::from_ptr(token_name).to_string_lossy();
+    let token_desc = CStr::from_ptr(token_desc).to_string_lossy();
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_mint_token(
+        builder_mut,
+        token_ptr,
+        &token_name,
+        &token_desc,
+        num_decimals,
+    )
+    .unwrap();
+}
+
+/// Add given token id and token amount
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_add_token(
+    builder_mut: ErgoBoxCandidateBuilderPtr,
+    token_id_ptr: ConstTokenIdPtr,
+    token_amount_ptr: ConstTokenAmountPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_builder_add_token(builder_mut, token_id_ptr, token_amount_ptr).unwrap();
+}
+
+/// Build the box candidate
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_builder_build(
+    builder_ptr: ConstErgoBoxCandidateBuilderPtr,
+    ergo_box_candidate_out: *mut ErgoBoxCandidatePtr,
+) -> ErrorPtr {
+    let res = ergo_box_candidate_builder_build(builder_ptr, ergo_box_candidate_out);
+    Error::c_api_from(res)
+}
+
+/// Drop `ErgoBoxCandidateBuilder`
+#[no_mangle]
+pub extern "C" fn ergo_lib_ergo_box_candidate_builder_delete(ptr: ErgoBoxCandidateBuilderPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/box_selector.rs b/bindings/ergo-lib-c/src/box_selector.rs
new file mode 100644
index 000000000..886a6186f
--- /dev/null
+++ b/bindings/ergo-lib-c/src/box_selector.rs
@@ -0,0 +1,92 @@
+use ergo_lib_c_core::{
+    box_selector::*,
+    collections::{CollectionPtr, ConstCollectionPtr},
+    ergo_box::{ConstBoxValuePtr, ErgoBox, ErgoBoxAssetsData},
+    token::ConstTokensPtr,
+    Error,
+};
+
+use paste::paste;
+
+use crate::{delete_ptr, ErrorPtr};
+
+// `BoxSelection` bindings -------------------------------------------------------------------------
+
+/// Create a selection to easily inject custom selection algorithms
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_selection_new(
+    ergo_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    change_ergo_boxes_ptr: ConstCollectionPtr<ErgoBoxAssetsData>,
+    box_selection_out: *mut BoxSelectionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    box_selection_new(ergo_boxes_ptr, change_ergo_boxes_ptr, box_selection_out).unwrap();
+}
+
+/// Selected boxes to spend as transaction inputs
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_selection_boxes(
+    box_selection_ptr: ConstBoxSelectionPtr,
+    ergo_boxes_out: *mut CollectionPtr<ErgoBox>,
+) {
+    #[allow(clippy::unwrap_used)]
+    box_selection_boxes(box_selection_ptr, ergo_boxes_out).unwrap();
+}
+
+/// Selected boxes to use as change
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_selection_change(
+    box_selection_ptr: ConstBoxSelectionPtr,
+    change_ergo_boxes_out: *mut CollectionPtr<ErgoBoxAssetsData>,
+) {
+    #[allow(clippy::unwrap_used)]
+    box_selection_change(box_selection_ptr, change_ergo_boxes_out).unwrap();
+}
+
+/// Drop `BoxSelection`
+#[no_mangle]
+pub extern "C" fn ergo_lib_box_selection_delete(ptr: BoxSelectionPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(BoxSelection);
+
+// `SimpleBoxSelector` bindings ---------------------------------------------------------------------
+
+/// Naive box selector, collects inputs until target balance is reached
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_simple_box_selector_new(
+    simple_box_selector_out: *mut SimpleBoxSelectorPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    simple_box_selector_new(simple_box_selector_out).unwrap();
+}
+
+/// Selects inputs to satisfy target balance and tokens.
+/// `inputs` - available inputs (returns an error, if empty),
+/// `target_balance` - coins (in nanoERGs) needed,
+/// `target_tokens` - amount of tokens needed.
+/// Returns selected inputs and box assets(value+tokens) with change.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_simple_box_selector_select(
+    simple_box_selector_ptr: ConstSimpleBoxSelectorPtr,
+    inputs_ptr: ConstCollectionPtr<ErgoBox>,
+    target_balance_ptr: ConstBoxValuePtr,
+    target_tokens_ptr: ConstTokensPtr,
+    box_selection_out: *mut BoxSelectionPtr,
+) -> ErrorPtr {
+    let res = simple_box_selector_select(
+        simple_box_selector_ptr,
+        inputs_ptr,
+        target_balance_ptr,
+        target_tokens_ptr,
+        box_selection_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Drop `SimpleBoxSelector`
+#[no_mangle]
+pub extern "C" fn ergo_lib_simple_box_selector_delete(ptr: SimpleBoxSelectorPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/byte_array.rs b/bindings/ergo-lib-c/src/byte_array.rs
new file mode 100644
index 000000000..f437479ff
--- /dev/null
+++ b/bindings/ergo-lib-c/src/byte_array.rs
@@ -0,0 +1,26 @@
+//! Byte arrays for use in proofs
+
+use ergo_lib_c_core::{
+    util::{byte_array_from_raw_parts, ByteArray, ByteArrayPtr, ConstByteArrayPtr},
+    Error, ErrorPtr,
+};
+use paste::paste;
+
+use crate::delete_ptr;
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_byte_array_from_raw_parts(
+    ptr: *const u8,
+    len: usize,
+    byte_array_out: *mut ByteArrayPtr,
+) -> ErrorPtr {
+    let res = byte_array_from_raw_parts(ptr, len, byte_array_out);
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub extern "C" fn ergo_lib_byte_array_delete(ptr: ByteArrayPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(ByteArrays, ByteArray);
diff --git a/bindings/ergo-lib-c/src/constant.rs b/bindings/ergo-lib-c/src/constant.rs
new file mode 100644
index 000000000..98aff96ca
--- /dev/null
+++ b/bindings/ergo-lib-c/src/constant.rs
@@ -0,0 +1,169 @@
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+};
+
+use ergo_lib_c_core::{
+    constant::*,
+    ergo_box::{ConstErgoBoxPtr, ErgoBoxPtr},
+    Error, ErrorPtr,
+};
+use paste::paste;
+
+use crate::{delete_ptr, ReturnNum};
+
+/// Decode from Base16-encoded ErgoTree serialized value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_base16(
+    bytes_ptr: *const c_char,
+    constant_out: *mut ConstantPtr,
+) -> ErrorPtr {
+    let bytes_str = CStr::from_ptr(bytes_ptr).to_string_lossy();
+    let res = constant_from_base16_bytes(&bytes_str, constant_out);
+    Error::c_api_from(res)
+}
+
+/// Encode as Base16-encoded ErgoTree serialized value or return an error if serialization
+/// failed
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_to_base16(
+    constant_ptr: ConstConstantPtr,
+    _bytes_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match constant_to_base16_str(constant_ptr) {
+        Ok(s) => {
+            *_bytes_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Create from i32 value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_i32(value: i32, constant_out: *mut ConstantPtr) {
+    #[allow(clippy::unwrap_used)]
+    constant_from_i32(value, constant_out).unwrap();
+}
+
+/// Extract i32 value, returning error if wrong type
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_to_i32(
+    constant_ptr: ConstConstantPtr,
+) -> ReturnNum<i32> {
+    match constant_to_i32(constant_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Create from i64
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_i64(value: i64, constant_out: *mut ConstantPtr) {
+    #[allow(clippy::unwrap_used)]
+    constant_from_i64(value, constant_out).unwrap();
+}
+
+/// Extract i64 value, returning error if wrong type
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_to_i64(
+    constant_ptr: ConstConstantPtr,
+) -> ReturnNum<i64> {
+    match constant_to_i64(constant_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Create from byte array
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_bytes(
+    bytes_ptr: *const u8,
+    len: usize,
+    constant_out: *mut ConstantPtr,
+) -> ErrorPtr {
+    let res = constant_from_bytes(bytes_ptr, len, constant_out);
+    Error::c_api_from(res)
+}
+
+/// Extract byte array len, returning error if wrong type
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_bytes_len(
+    constant_ptr: ConstConstantPtr,
+) -> ReturnNum<usize> {
+    match constant_bytes_len(constant_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Extract byte array, returning error if wrong type.  **Key assumption:** enough memory has been
+/// allocated at the address pointed-to by `output`. Use `ergo_lib_constant_bytes_len` to
+/// determine the length of the byte array.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_to_bytes(
+    constant_ptr: ConstConstantPtr,
+    output: *mut u8,
+) -> ErrorPtr {
+    let res = constant_to_bytes(constant_ptr, output);
+    Error::c_api_from(res)
+}
+
+/// Parse raw [`EcPoint`] value from bytes and make [`ProveDlog`] constant
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_ecpoint_bytes(
+    bytes_ptr: *const u8,
+    len: usize,
+    constant_out: *mut ConstantPtr,
+) -> ErrorPtr {
+    let res = constant_from_ecpoint_bytes(bytes_ptr, len, constant_out);
+    Error::c_api_from(res)
+}
+
+/// Create from ErgoBox value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_ergo_box(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    constant_out: *mut ConstantPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    constant_from_ergo_box(ergo_box_ptr, constant_out).unwrap();
+}
+
+/// Extract ErgoBox value, returning error if wrong type
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_to_ergo_box(
+    constant_ptr: ConstConstantPtr,
+    ergo_box_out: *mut ErgoBoxPtr,
+) -> ErrorPtr {
+    let res = constant_to_ergo_box(constant_ptr, ergo_box_out);
+    Error::c_api_from(res)
+}
+
+/// Drop `Constant`
+#[no_mangle]
+pub extern "C" fn ergo_lib_constant_delete(ptr: ConstantPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(Constant);
diff --git a/bindings/ergo-lib-c/src/context_extension.rs b/bindings/ergo-lib-c/src/context_extension.rs
new file mode 100644
index 000000000..31104757d
--- /dev/null
+++ b/bindings/ergo-lib-c/src/context_extension.rs
@@ -0,0 +1,37 @@
+use ergo_lib_c_core::context_extension::*;
+
+use crate::delete_ptr;
+
+/// Create new empty ContextExtension instance
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_context_extension_empty(
+    context_extension_out: *mut ContextExtensionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    context_extension_empty(context_extension_out).unwrap();
+}
+
+/// Returns the number of elements in the collection
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_context_extension_len(
+    context_extension_ptr: ConstContextExtensionPtr,
+) -> usize {
+    #[allow(clippy::unwrap_used)]
+    context_extension_len(context_extension_ptr).unwrap()
+}
+
+/// Returns all keys (represented as u8 values) in the map
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_context_extension_keys(
+    context_extension_ptr: ConstContextExtensionPtr,
+    output: *mut u8,
+) {
+    #[allow(clippy::unwrap_used)]
+    context_extension_keys(context_extension_ptr, output).unwrap();
+}
+
+/// Drop `ContextExtension`
+#[no_mangle]
+pub extern "C" fn ergo_lib_context_extension_delete(ptr: ContextExtensionPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/contract.rs b/bindings/ergo-lib-c/src/contract.rs
new file mode 100644
index 000000000..59994c7a0
--- /dev/null
+++ b/bindings/ergo-lib-c/src/contract.rs
@@ -0,0 +1,61 @@
+//! Contract, for easier ErgoTree generation
+use ergo_lib_c_core::{
+    address::ConstAddressPtr,
+    contract::*,
+    ergo_tree::{ConstErgoTreePtr, ErgoTreePtr},
+    Error, ErrorPtr,
+};
+use paste::paste;
+
+use std::{ffi::CStr, os::raw::c_char};
+
+use crate::delete_ptr;
+
+/// Create new contract from ErgoTree
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_contract_new(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    contract_out: *mut ContractPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    contract_new(ergo_tree_ptr, contract_out).unwrap();
+}
+
+/// Create new contract that allow spending of the guarded box by a given recipient ([`Address`])
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_contract_pay_to_address(
+    address_ptr: ConstAddressPtr,
+    contract_out: *mut ContractPtr,
+) -> ErrorPtr {
+    let res = contract_pay_to_address(address_ptr, contract_out);
+    Error::c_api_from(res)
+}
+
+/// Compiles a contract from ErgoScript source code
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_contract_compile(
+    source: *const c_char,
+    contract_out: *mut ContractPtr,
+) -> ErrorPtr {
+    let source = CStr::from_ptr(source).to_string_lossy();
+    let res = contract_compile(&source, contract_out);
+    Error::c_api_from(res)
+}
+
+/// Get the ErgoTree of the contract
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_contract_ergo_tree(
+    contract_ptr: ConstContractPtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    contract_ergo_tree(contract_ptr, ergo_tree_out).unwrap();
+}
+
+/// Drop `Contract`
+#[no_mangle]
+pub extern "C" fn ergo_lib_contract_delete(ptr: ContractPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(Contract);
diff --git a/bindings/ergo-lib-c/src/data_input.rs b/bindings/ergo-lib-c/src/data_input.rs
new file mode 100644
index 000000000..a6bedf291
--- /dev/null
+++ b/bindings/ergo-lib-c/src/data_input.rs
@@ -0,0 +1,37 @@
+//! DataInput type
+use ergo_lib_c_core::{
+    data_input::*,
+    ergo_box::{BoxIdPtr, ConstBoxIdPtr},
+    Error,
+};
+
+use crate::delete_ptr;
+use paste::paste;
+
+/// Parse box id (32 byte digest)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_data_input_new(
+    box_id_ptr: ConstBoxIdPtr,
+    data_input_out: *mut DataInputPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    data_input_new(box_id_ptr, data_input_out).unwrap();
+}
+
+/// Get box id
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_data_input_box_id(
+    data_input_ptr: ConstDataInputPtr,
+    box_id_out: *mut BoxIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    data_input_box_id(data_input_ptr, box_id_out).unwrap();
+}
+
+/// Drop `DataInput`
+#[no_mangle]
+pub extern "C" fn ergo_lib_data_input_delete(ptr: DataInputPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(DataInputs, DataInput);
diff --git a/bindings/ergo-lib-c/src/ergo_box.rs b/bindings/ergo-lib-c/src/ergo_box.rs
new file mode 100644
index 000000000..750dad47d
--- /dev/null
+++ b/bindings/ergo-lib-c/src/ergo_box.rs
@@ -0,0 +1,392 @@
+//! Box (aka coin, or an unspent output) is a basic concept of a UTXO-based cryptocurrency.
+//! In Bitcoin, such an object is associated with some monetary value (arbitrary,
+//! but with predefined precision, so we use integer arithmetic to work with the value),
+//! and also a guarding script (aka proposition) to protect the box from unauthorized opening.
+//!
+//! In other way, a box is a state element locked by some proposition (ErgoTree).
+//!
+//! In Ergo, box is just a collection of registers, some with mandatory types and semantics,
+//! others could be used by applications in any way.
+//! We add additional fields in addition to amount and proposition~(which stored in the registers R0 and R1).
+//! Namely, register R2 contains additional tokens (a sequence of pairs (token identifier, value)).
+//! Register R3 contains height when block got included into the blockchain and also transaction
+//! identifier and box index in the transaction outputs.
+//! Registers R4-R9 are free for arbitrary usage.
+//!
+//! A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
+//! can not be linked to the same box.
+use ergo_lib_c_core::{
+    constant::ConstantPtr,
+    contract::ConstContractPtr,
+    ergo_box::*,
+    ergo_tree::ErgoTreePtr,
+    token::{ConstTokensPtr, TokensPtr},
+    transaction::ConstTxIdPtr,
+    Error,
+};
+use paste::paste;
+
+use crate::{ErrorPtr, ReturnOption};
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+};
+
+use crate::delete_ptr;
+
+// `BoxId` bindings --------------------------------------------------------------------------------
+
+/// Parse box id (32 byte digest) from base16-encoded string
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_id_from_str(
+    box_id_str: *const c_char,
+    box_id_out: *mut BoxIdPtr,
+) -> ErrorPtr {
+    let box_id_str = CStr::from_ptr(box_id_str).to_string_lossy();
+    let res = box_id_from_str(&box_id_str, box_id_out);
+    Error::c_api_from(res)
+}
+
+/// Base16 encoded string
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_id_to_str(
+    box_id_ptr: ConstBoxIdPtr,
+    _box_id_str: *mut *const c_char,
+) {
+    #[allow(clippy::unwrap_used)]
+    {
+        let s = box_id_to_str(box_id_ptr).unwrap();
+        *_box_id_str = CString::new(s).unwrap().into_raw();
+    }
+}
+
+/// Returns byte array (32 bytes) Note: it's imperative that `output` points to a valid block of
+/// memory of 32 bytes.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_id_to_bytes(box_id_ptr: ConstBoxIdPtr, output: *mut u8) {
+    #[allow(clippy::unwrap_used)]
+    box_id_to_bytes(box_id_ptr, output).unwrap();
+}
+
+/// Drop `BoxId`
+#[no_mangle]
+pub extern "C" fn ergo_lib_box_id_delete(ptr: BoxIdPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(BoxId);
+
+// `BoxValue` bindings ------------------------------------------------------------------------------
+
+/// Recommended (safe) minimal box value to use in case box size estimation is unavailable.
+/// Allows box size upto 2777 bytes with current min box value per byte of 360 nanoERGs
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_value_safe_user_min(box_value_out: *mut BoxValuePtr) {
+    #[allow(clippy::unwrap_used)]
+    box_value_safe_user_min(box_value_out).unwrap();
+}
+
+/// Number of units inside one ERGO (i.e. one ERG using nano ERG representation)
+#[no_mangle]
+pub extern "C" fn ergo_lib_box_value_units_per_ergo() -> i64 {
+    box_value_units_per_ergo()
+}
+
+/// Create from i64 with bounds check
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_value_from_i64(
+    amount: i64,
+    box_value_out: *mut BoxValuePtr,
+) -> ErrorPtr {
+    let res = box_value_from_i64(amount, box_value_out);
+    Error::c_api_from(res)
+}
+
+/// Get value as signed 64-bit long
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_value_as_i64(box_value_ptr: ConstBoxValuePtr) -> i64 {
+    #[allow(clippy::unwrap_used)]
+    box_value_as_i64(box_value_ptr).unwrap()
+}
+
+/// Create a new box value which is the sum of the arguments, with bounds check.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_box_value_sum_of(
+    box_value0_ptr: ConstBoxValuePtr,
+    box_value1_ptr: ConstBoxValuePtr,
+    sum_of_out: *mut BoxValuePtr,
+) -> ErrorPtr {
+    let res = box_value_sum_of(box_value0_ptr, box_value1_ptr, sum_of_out);
+    Error::c_api_from(res)
+}
+
+/// Drop `BoxValue`
+#[no_mangle]
+pub extern "C" fn ergo_lib_box_value_delete(ptr: BoxValuePtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(BoxValue);
+
+// `ErgoBoxCandidate` bindings ----------------------------------------------------------------------
+
+/// Returns value (ErgoTree constant) stored in the register or None if the register is empty
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_register_value(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    register_id: NonMandatoryRegisterId,
+    constant_out: *mut ConstantPtr,
+) -> ReturnOption {
+    match ergo_box_candidate_register_value(ergo_box_candidate_ptr, register_id, constant_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Get box creation height
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_creation_height(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+) -> u32 {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_creation_height(ergo_box_candidate_ptr).unwrap()
+}
+
+/// Get tokens for box
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_tokens(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    tokens_out: *mut TokensPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_tokens(ergo_box_candidate_ptr, tokens_out).unwrap();
+}
+
+/// Get ergo tree for box
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_ergo_tree(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_ergo_tree(ergo_box_candidate_ptr, ergo_tree_out).unwrap();
+}
+
+/// Get box value in nanoERGs
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_box_value(
+    ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
+    box_value_out: *mut BoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_candidate_box_value(ergo_box_candidate_ptr, box_value_out).unwrap();
+}
+
+/// Drop `ErgoBoxCandidate`
+#[no_mangle]
+pub extern "C" fn ergo_lib_ergo_box_candidate_delete(ptr: ErgoBoxCandidatePtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(ErgoBoxCandidates, ErgoBoxCandidate);
+make_ffi_eq!(ErgoBoxCandidate);
+
+// `ErgoBox` bindings ------------------------------------------------------------------------------
+
+/// Make a new box with:
+/// `value` - amount of money associated with the box
+/// `contract` - guarding contract([`Contract`]), which should be evaluated to true in order
+/// to open(spend) this box
+/// `creation_height` - height when a transaction containing the box is created.
+/// `tx_id` - transaction id in which this box was "created" (participated in outputs)
+/// `index` - index (in outputs) in the transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_new(
+    value_ptr: ConstBoxValuePtr,
+    creation_height: u32,
+    contract_ptr: ConstContractPtr,
+    tx_id_ptr: ConstTxIdPtr,
+    index: u16,
+    tokens_ptr: ConstTokensPtr,
+    ergo_box_out: *mut ErgoBoxPtr,
+) -> ErrorPtr {
+    let res = ergo_box_new(
+        value_ptr,
+        creation_height,
+        contract_ptr,
+        tx_id_ptr,
+        index,
+        tokens_ptr,
+        ergo_box_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Get box id
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_id(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    box_id_out: *mut BoxIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_box_id(ergo_box_ptr, box_id_out).unwrap();
+}
+
+/// Get box creation height
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_creation_height(ergo_box_ptr: ConstErgoBoxPtr) -> u32 {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_creation_height(ergo_box_ptr).unwrap()
+}
+
+/// Get tokens for box
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_tokens(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    tokens_out: *mut TokensPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_tokens(ergo_box_ptr, tokens_out).unwrap();
+}
+
+/// Get ergo tree for box
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_ergo_tree(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_ergo_tree(ergo_box_ptr, ergo_tree_out).unwrap();
+}
+
+/// Get box value in nanoERGs
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_value(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    box_value_out: *mut BoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_value(ergo_box_ptr, box_value_out).unwrap();
+}
+
+/// Returns value (ErgoTree constant) stored in the register or None if the register is empty
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_register_value(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    register_id: NonMandatoryRegisterId,
+    constant_out: *mut ConstantPtr,
+) -> ReturnOption {
+    match ergo_box_register_value(ergo_box_ptr, register_id, constant_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Parse from JSON.  Supports Ergo Node/Explorer API and box values and token amount encoded as
+/// strings
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_from_json(
+    json_str: *const c_char,
+    ergo_box_out: *mut ErgoBoxPtr,
+) -> ErrorPtr {
+    let json = CStr::from_ptr(json_str).to_string_lossy();
+    let res = ergo_box_from_json(&json, ergo_box_out);
+    Error::c_api_from(res)
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_to_json(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match ergo_box_to_json(ergo_box_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_to_json_eip12(
+    ergo_box_ptr: ConstErgoBoxPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match ergo_box_to_json_eip12(ergo_box_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Drop `ErgoBox`
+#[no_mangle]
+pub extern "C" fn ergo_lib_ergo_box_delete(ptr: ErgoBoxPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(ErgoBoxes, ErgoBox);
+make_ffi_eq!(ErgoBox);
+
+// `ErgoBoxAssetsData` bindings ---------------------------------------------------------------------
+
+/// Create new instance
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_assets_data_new(
+    value_ptr: ConstBoxValuePtr,
+    tokens_ptr: ConstTokensPtr,
+    ergo_box_assets_data_out: *mut ErgoBoxAssetsDataPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_assets_data_new(value_ptr, tokens_ptr, ergo_box_assets_data_out).unwrap();
+}
+
+/// Value part of the box
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_assets_data_value(
+    ergo_box_assets_data_ptr: ConstErgoBoxAssetsDataPtr,
+    value_out: *mut BoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_assets_data_value(ergo_box_assets_data_ptr, value_out).unwrap();
+}
+
+/// Tokens part of the box
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_assets_data_tokens(
+    ergo_box_assets_data_ptr: ConstErgoBoxAssetsDataPtr,
+    tokens_out: *mut TokensPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_assets_data_tokens(ergo_box_assets_data_ptr, tokens_out).unwrap();
+}
+
+/// Drop `ErgoBoxAssetsData`
+#[no_mangle]
+pub extern "C" fn ergo_lib_ergo_box_assets_data_delete(ptr: ErgoBoxAssetsDataPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(ErgoBoxAssetsDataList, ErgoBoxAssetsData);
+make_ffi_eq!(ErgoBoxAssetsData);
diff --git a/bindings/ergo-lib-c/src/ergo_state_ctx.rs b/bindings/ergo-lib-c/src/ergo_state_ctx.rs
new file mode 100644
index 000000000..23fd546fb
--- /dev/null
+++ b/bindings/ergo-lib-c/src/ergo_state_ctx.rs
@@ -0,0 +1,26 @@
+/// Blockchain state (last headers, etc.)
+use crate::{block_header::ConstBlockHeadersPtr, delete_ptr, ErrorPtr};
+use ergo_lib_c_core::{
+    ergo_state_ctx::{ergo_state_context_new, ConstErgoStateContextPtr, ErgoStateContextPtr},
+    header::ConstPreHeaderPtr,
+    Error,
+};
+use paste::paste;
+
+/// Create new context from pre-header
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_state_context_new(
+    pre_header_ptr: ConstPreHeaderPtr,
+    headers: ConstBlockHeadersPtr,
+    ergo_state_context_out: *mut ErgoStateContextPtr,
+) -> ErrorPtr {
+    let res = ergo_state_context_new(pre_header_ptr, headers, ergo_state_context_out);
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_state_context_delete(ptr: ErgoStateContextPtr) {
+    delete_ptr(ptr)
+}
+
+make_ffi_eq!(ErgoStateContext);
diff --git a/bindings/ergo-lib-c/src/ergo_tree.rs b/bindings/ergo-lib-c/src/ergo_tree.rs
new file mode 100644
index 000000000..4bdfb2a05
--- /dev/null
+++ b/bindings/ergo-lib-c/src/ergo_tree.rs
@@ -0,0 +1,174 @@
+//! ErgoTree
+
+use ergo_lib_c_core::{
+    constant::{ConstConstantPtr, ConstantPtr},
+    ergo_tree::*,
+    Error,
+};
+use paste::paste;
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+};
+
+use crate::{delete_ptr, ErrorPtr, ReturnNum, ReturnOption};
+
+/// Decode from base16 encoded serialized ErgoTree
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_from_base16_bytes(
+    bytes_ptr: *const c_char,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> ErrorPtr {
+    let bytes_str = CStr::from_ptr(bytes_ptr).to_string_lossy();
+    let res = ergo_tree_from_base16_bytes(&bytes_str, ergo_tree_out);
+    Error::c_api_from(res)
+}
+
+/// Decode from encoded serialized ErgoTree
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_from_bytes(
+    bytes_ptr: *const u8,
+    len: usize,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> ErrorPtr {
+    let res = ergo_tree_from_bytes(bytes_ptr, len, ergo_tree_out);
+    Error::c_api_from(res)
+}
+
+/// Determine number of bytes of the serialized ErgoTree
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_bytes_len(
+    ergo_tree_ptr: ConstErgoTreePtr,
+) -> ReturnNum<usize> {
+    match ergo_tree_bytes_len(ergo_tree_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Convert to serialized bytes. **Key assumption:** enough memory has been allocated at the address
+/// pointed-to by `output`. Use `ergo_lib_ergo_tree_bytes_len` to determine the length of the
+/// byte array.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_to_bytes(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    output: *mut u8,
+) -> ErrorPtr {
+    let res = ergo_tree_to_bytes(ergo_tree_ptr, output);
+    Error::c_api_from(res)
+}
+
+/// Get Base16-encoded serialized bytes
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_to_base16_bytes(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    _str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match ergo_tree_to_base16_bytes(ergo_tree_ptr) {
+        Ok(s) => {
+            *_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Get constants number as stored in serialized ErgoTree or error if the parsing of
+/// constants is failed
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_constants_len(
+    ergo_tree_ptr: ConstErgoTreePtr,
+) -> ReturnNum<usize> {
+    match ergo_tree_constants_len(ergo_tree_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Returns constant with given index (as stored in serialized ErgoTree)
+/// or None if index is out of bounds
+/// or error if constants parsing were failed
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_get_constant(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    index: usize,
+    constant_out: *mut ConstantPtr,
+) -> ReturnOption {
+    match ergo_tree_get_constant(ergo_tree_ptr, index, constant_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Returns new ErgoTree instance with a new constant value for a given index in constants list (as
+/// stored in serialized ErgoTree), or an error. Note that the original ErgoTree instance
+/// pointed-at by `ergo_tree_ptr` is untouched.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_with_constant(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    index: usize,
+    constant_ptr: ConstConstantPtr,
+    ergo_tree_out: *mut ErgoTreePtr,
+) -> ErrorPtr {
+    let res = ergo_tree_with_constant(ergo_tree_ptr, index, constant_ptr, ergo_tree_out);
+    Error::c_api_from(res)
+}
+
+/// Returns the number of bytes of the Serialized proposition expression of SigmaProp type with
+/// ConstantPlaceholder nodes instead of Constant nodes.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_template_bytes_len(
+    ergo_tree_ptr: ConstErgoTreePtr,
+) -> ReturnNum<usize> {
+    match ergo_tree_template_bytes_len(ergo_tree_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Serialized proposition expression of SigmaProp type with ConstantPlaceholder nodes instead of
+/// Constant nodes. Key assumption: enough memory has been allocated at the address pointed-to by
+/// `output`. Use `ergo_lib_ergo_tree_template_bytes_len` to determine the length of the byte
+/// array.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_tree_template_bytes(
+    ergo_tree_ptr: ConstErgoTreePtr,
+    output: *mut u8,
+) -> ErrorPtr {
+    let res = ergo_tree_template_bytes(ergo_tree_ptr, output);
+    Error::c_api_from(res)
+}
+
+/// Drop `ErgoTree`
+#[no_mangle]
+pub extern "C" fn ergo_lib_ergo_tree_delete(ptr: ErgoTreePtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(ErgoTree);
diff --git a/bindings/ergo-lib-c/src/header.rs b/bindings/ergo-lib-c/src/header.rs
new file mode 100644
index 000000000..aad17deed
--- /dev/null
+++ b/bindings/ergo-lib-c/src/header.rs
@@ -0,0 +1,27 @@
+//! Block header with the current `spendingTransaction`, that can be predicted by a miner before its
+//! formation
+use ergo_lib_c_core::{
+    block_header::ConstBlockHeaderPtr,
+    header::{preheader_from_block_header, ConstPreHeaderPtr, PreHeaderPtr},
+};
+use paste::paste;
+
+use crate::delete_ptr;
+
+/// Create instance using data from block header
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_preheader_from_block_header(
+    block_header: ConstBlockHeaderPtr,
+    preheader_out: *mut PreHeaderPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    preheader_from_block_header(block_header, preheader_out).unwrap();
+}
+
+/// Drop `PreHeader`
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_preheader_delete(ptr: PreHeaderPtr) {
+    delete_ptr(ptr)
+}
+
+make_ffi_eq!(PreHeader);
diff --git a/bindings/ergo-lib-c/src/input.rs b/bindings/ergo-lib-c/src/input.rs
new file mode 100644
index 000000000..646eea079
--- /dev/null
+++ b/bindings/ergo-lib-c/src/input.rs
@@ -0,0 +1,125 @@
+//! Ergo input
+use std::{ffi::CString, os::raw::c_char};
+
+use ergo_lib_c_core::{
+    context_extension::ContextExtensionPtr, ergo_box::BoxIdPtr, input::*, Error,
+};
+
+use crate::{delete_ptr, ErrorPtr};
+use paste::paste;
+
+// `UnsignedInput` bindings -------------------------------------------------------------------------
+
+/// Get box id
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_input_box_id(
+    unsigned_input_ptr: ConstUnsignedInputPtr,
+    box_id_out: *mut BoxIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    unsigned_input_box_id(unsigned_input_ptr, box_id_out).unwrap();
+}
+
+/// Get extension
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_input_context_extension(
+    unsigned_input_ptr: ConstUnsignedInputPtr,
+    context_extension_out: *mut ContextExtensionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    unsigned_input_context_extension(unsigned_input_ptr, context_extension_out).unwrap();
+}
+
+/// Drop `UnsignedInput`
+#[no_mangle]
+pub extern "C" fn ergo_lib_unsigned_input_delete(ptr: UnsignedInputPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(UnsignedInputs, UnsignedInput);
+
+// `Input` bindings --------------------------------------------------------------------------------
+
+/// Get box id
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_input_box_id(
+    input_ptr: ConstInputPtr,
+    box_id_out: *mut BoxIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    input_box_id(input_ptr, box_id_out).unwrap();
+}
+
+/// Get spending proof
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_input_spending_proof(
+    input_ptr: ConstInputPtr,
+    prover_result_out: *mut ProverResultPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    input_spending_proof(input_ptr, prover_result_out).unwrap();
+}
+
+/// Drop `Input`
+#[no_mangle]
+pub extern "C" fn ergo_lib_input_delete(ptr: InputPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(Inputs, Input);
+
+// `ProverResult` bindings -------------------------------------------------------------------------
+
+/// Return the number of bytes that make up the proof.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_prover_result_proof_len(
+    prover_result_ptr: ConstProverResultPtr,
+) -> usize {
+    #[allow(clippy::unwrap_used)]
+    prover_result_proof_len(prover_result_ptr).unwrap()
+}
+
+/// Get proof. Key assumption: enough memory has been allocated at the address pointed-to by
+/// `output`. Use `ergo_lib_prover_result_proof_len` to determine the length of the byte
+/// array.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_prover_result_proof(
+    prover_result_ptr: ConstProverResultPtr,
+    output: *mut u8,
+) {
+    #[allow(clippy::unwrap_used)]
+    prover_result_proof(prover_result_ptr, output).unwrap();
+}
+
+/// Get extension
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_prover_result_context_extension(
+    prover_result_ptr: ConstProverResultPtr,
+    context_extension_out: *mut ContextExtensionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    prover_result_context_extension(prover_result_ptr, context_extension_out).unwrap();
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_prover_result_to_json(
+    prover_result_ptr: ConstProverResultPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match prover_result_to_json(prover_result_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Drop `ProverResult`
+#[no_mangle]
+pub extern "C" fn ergo_lib_prover_result_delete(ptr: ProverResultPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/lib.rs b/bindings/ergo-lib-c/src/lib.rs
index 71b9adf90..466257b3e 100644
--- a/bindings/ergo-lib-c/src/lib.rs
+++ b/bindings/ergo-lib-c/src/lib.rs
@@ -7,160 +7,63 @@
 #![deny(unused_mut)]
 #![deny(dead_code)]
 #![deny(unused_imports)]
+#![deny(clippy::unwrap_used)]
+#![deny(clippy::expect_used)]
 // #![deny(missing_docs)]
 #![allow(clippy::missing_safety_doc)]
 
-use ergo_lib::ergotree_ir::chain;
-
-use ergo_lib_c_core::{address_delete, address_from_testnet};
-pub use ergo_lib_c_core::{Address, Error};
-use std::{
-    ffi::{CStr, CString},
-    os::raw::c_char,
+#[macro_use]
+mod macros;
+mod address;
+mod block_header;
+mod box_builder;
+mod box_selector;
+mod byte_array;
+mod constant;
+mod context_extension;
+mod contract;
+mod data_input;
+mod ergo_box;
+mod ergo_state_ctx;
+mod ergo_tree;
+mod header;
+mod input;
+mod reduced;
+mod secret_key;
+mod token;
+mod transaction;
+mod tx_builder;
+mod wallet;
+
+pub use crate::address::*;
+pub use crate::block_header::*;
+pub use crate::box_builder::*;
+pub use crate::box_selector::*;
+pub use crate::byte_array::*;
+pub use crate::context_extension::*;
+pub use crate::contract::*;
+pub use crate::data_input::*;
+pub use crate::ergo_box::*;
+pub use crate::ergo_state_ctx::*;
+pub use crate::ergo_tree::*;
+pub use crate::header::*;
+pub use crate::input::*;
+pub use crate::reduced::*;
+pub use crate::secret_key::*;
+pub use crate::token::*;
+pub use crate::transaction::*;
+pub use crate::tx_builder::*;
+pub use crate::wallet::*;
+pub use ergo_lib_c_core::{
+    address::{Address, AddressTypePrefix, NetworkPrefix},
+    Error,
 };
+use std::{ffi::CString, os::raw::c_char};
 
 pub type ErrorPtr = *mut Error;
-pub type AddressPtr = *mut Address;
-
-pub struct ErgoStateContext(ergo_lib::chain::ergo_state_context::ErgoStateContext);
-pub type ErgoStateContextPtr = *mut ErgoStateContext;
-
-#[no_mangle]
-pub unsafe extern "C" fn ergo_wallet_ergo_state_context_from_json(
-    _json_str: *const c_char,
-    _ergo_state_context_out: *mut ErgoStateContextPtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn ergo_wallet_ergo_state_context_delete(
-    _ergo_state_context: ErgoStateContextPtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-pub struct ErgoBoxCandidate(chain::ergo_box::ErgoBoxCandidate);
-pub type ErgoBoxCandidatePtr = *mut ErgoBoxCandidate;
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_ergo_box_candidate_new_pay_to_address(
-    _recipient: AddressPtr,
-    _value: u64,
-    _creation_height: u32,
-    _ergo_box_candidate_out: *mut ErgoBoxCandidatePtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_ergo_box_candidate_delete(
-    _ergo_box_candidate: ErgoBoxCandidatePtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn ergo_wallet_address_from_testnet(
-    address_str: *const c_char,
-    address_out: *mut AddressPtr,
-) -> ErrorPtr {
-    let address = CStr::from_ptr(address_str).to_string_lossy();
-    let res = address_from_testnet(&address, address_out);
-    Error::c_api_from(res)
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_address_delete(address: AddressPtr) {
-    address_delete(address)
-}
-
-pub struct UnspentBoxes(Vec<chain::ergo_box::ErgoBoxCandidate>);
-pub type UnspentBoxesPtr = *mut UnspentBoxes;
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_unspent_boxes_from_json(
-    _json_str: *const c_char,
-    _unspent_boxes_out: *mut UnspentBoxesPtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_unspent_boxes_delete(_unspent_boxes: UnspentBoxesPtr) -> ErrorPtr {
-    todo!()
-}
-
-pub struct DataInputBoxes(Vec<chain::ergo_box::ErgoBoxCandidate>);
-pub type DataInputBoxesPtr = *mut DataInputBoxes;
-
-pub struct OutputBoxes(Vec<chain::ergo_box::ErgoBoxCandidate>);
-pub type OutputBoxesPtr = *mut OutputBoxes;
 
 #[no_mangle]
-pub extern "C" fn ergo_wallet_output_boxes_new(
-    _ergo_box_candidate: ErgoBoxCandidatePtr,
-    _output_boxes_out: *mut OutputBoxesPtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_output_boxes_delete(_output_boxes: OutputBoxesPtr) -> ErrorPtr {
-    todo!()
-}
-
-pub struct Wallet();
-pub type WalletPtr = *mut Wallet;
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_wallet_from_mnemonic(
-    _mnemonic_phrase: *const c_char,
-    _mnemonic_password: *const u8,
-    _mnemonic_password_length: usize,
-    _wallet_out: *mut WalletPtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_wallet_delete(_wallet: WalletPtr) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_wallet_new_signed_tx(
-    _wallet: WalletPtr,
-    _state_context: ErgoStateContextPtr,
-    _unspent_boxes: UnspentBoxesPtr,
-    _data_input_boxes: DataInputBoxesPtr, // can be null
-    _output_boxes: OutputBoxesPtr,
-    _send_change_to: AddressPtr,
-    _min_change_value: u64,
-    _tx_fee_amount: u64,
-    _transaction_out: *mut TransactionPtr,
-) -> ErrorPtr {
-    todo!()
-}
-
-pub struct Transaction(ergo_lib::chain::transaction::Transaction);
-pub type TransactionPtr = *mut Transaction;
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_delete_signed_tx(_transaction: TransactionPtr) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub extern "C" fn ergo_wallet_signed_tx_to_json(
-    _transaction: TransactionPtr,
-    _json_str_out: *mut *const c_char,
-) -> ErrorPtr {
-    todo!()
-}
-
-#[no_mangle]
-pub unsafe extern "C" fn ergo_wallet_delete_string(ptr: *mut c_char) {
+pub unsafe extern "C" fn ergo_lib_delete_string(ptr: *mut c_char) {
     if !ptr.is_null() {
         let cstring = CString::from_raw(ptr);
         std::mem::drop(cstring)
@@ -168,7 +71,7 @@ pub unsafe extern "C" fn ergo_wallet_delete_string(ptr: *mut c_char) {
 }
 
 #[no_mangle]
-pub extern "C" fn ergo_wallet_delete_error(error: ErrorPtr) {
+pub extern "C" fn ergo_lib_delete_error(error: ErrorPtr) {
     if !error.is_null() {
         let boxed = unsafe { Box::from_raw(error) };
         std::mem::drop(boxed);
@@ -176,10 +79,41 @@ pub extern "C" fn ergo_wallet_delete_error(error: ErrorPtr) {
 }
 
 #[no_mangle]
-pub unsafe extern "C" fn ergo_wallet_error_to_string(error: ErrorPtr) -> *mut c_char {
+pub unsafe extern "C" fn ergo_lib_error_to_string(error: ErrorPtr) -> *mut c_char {
+    #[allow(clippy::unwrap_used)]
     if let Some(error) = error.as_ref() {
         CString::new(error.to_string()).unwrap().into_raw()
     } else {
         CString::new(b"success".to_vec()).unwrap().into_raw()
     }
 }
+
+/// Convenience type to allow us to pass Rust enums with `u8` representation through FFI to the C
+/// side.
+#[repr(C)]
+pub struct ReturnNum<T: IntegerType> {
+    /// Returned value. Note that it's only valid if the error field is null!
+    value: T,
+    error: ErrorPtr,
+}
+
+/// Convenience type to allow us to pass Rust `Option<_>` types through FFI to C side.
+#[repr(C)]
+pub struct ReturnOption {
+    is_some: bool,
+    error: ErrorPtr,
+}
+
+pub unsafe fn delete_ptr<T>(ptr: *mut T) {
+    if !ptr.is_null() {
+        let boxed = Box::from_raw(ptr);
+        std::mem::drop(boxed);
+    }
+}
+pub trait IntegerType {}
+
+impl IntegerType for u8 {}
+impl IntegerType for i32 {}
+impl IntegerType for u32 {}
+impl IntegerType for i64 {}
+impl IntegerType for usize {}
diff --git a/bindings/ergo-lib-c/src/macros.rs b/bindings/ergo-lib-c/src/macros.rs
new file mode 100644
index 000000000..7a7a67c3c
--- /dev/null
+++ b/bindings/ergo-lib-c/src/macros.rs
@@ -0,0 +1,139 @@
+#[allow(unused_imports)]
+use paste::paste;
+
+/// `ergo-lib` uses a number of collection types that are simple wrappers around `Vec`. We have a
+/// generic type for such a collection in `ergo-lib-c-core::collections::Collection`. A limitation
+/// of `cbindgen` is that it cannot process generic functions. This macro generates a C-compatible
+/// interface for such a collection for any desired type T.
+///
+/// As an example the call `make_collection(BlockHeaders, BlockHeader);` generates:
+///
+///```
+///pub type BlockHeadersPtr = CollectionPtr<BlockHeader>;
+///pub type ConstBlockHeadersPtr = ConstCollectionPtr<BlockHeader>;
+///
+///#[no_mangle]
+///pub unsafe extern "C" fn ergo_lib_block_headers_new(collection_ptr_out: *mut BlockHeadersPtr) {
+///    #[allow(clippy::unwrap_used)]
+///    collection_new(collection_ptr_out).unwrap();
+///}
+///
+///#[no_mangle]
+///pub unsafe extern "C" fn ergo_lib_block_headers_delete(collection_ptr_out: BlockHeadersPtr) {
+///    collection_delete(collection_ptr_out)
+///}
+///
+///#[no_mangle]
+///pub unsafe extern "C" fn ergo_lib_block_headers_len(
+///    collection_ptr: ConstBlockHeadersPtr,
+///) -> usize {
+///    #[allow(clippy::unwrap_used)]
+///    collection_len(collection_ptr).unwrap()
+///}
+///
+///#[no_mangle]
+///pub unsafe extern "C" fn ergo_lib_block_headers_get(
+///    collection_ptr: ConstBlockHeadersPtr,
+///    index: usize,
+///    element_ptr_out: *mut BlockHeaderPtr,
+///) -> ReturnOption {
+///    match collection_get(collection_ptr, index, element_ptr_out) {
+///       Ok(is_some) => crate::ReturnOption {
+///           is_some,
+///           error: std::ptr::null_mut(),
+///       },
+///       Err(e) => crate::ReturnOption {
+///           is_some: false, // Just a dummy value
+///           error: Error::c_api_from(Err(e)),
+///       },
+///    }
+///}
+///
+///#[no_mangle]
+///pub unsafe extern "C" fn ergo_lib_block_headers_add(
+///    element_ptr: ConstBlockHeaderPtr,
+///    collection_ptr_out: BlockHeadersPtr,
+///) {
+///    #[allow(clippy::unwrap_used)]
+///    collection_add(collection_ptr_out, element_ptr).unwrap();
+///}
+///```
+#[macro_export]
+macro_rules! make_collection {
+    ($collection_type_name:ident, $item_type_name:ident) => {
+        paste! {
+            pub type [<$collection_type_name Ptr>] = ergo_lib_c_core::collections::CollectionPtr<$item_type_name>;
+            pub type [<Const $collection_type_name Ptr>] = ergo_lib_c_core::collections::ConstCollectionPtr<$item_type_name>;
+
+            /// Create a new empty collection
+            #[no_mangle]
+            pub unsafe extern "C" fn [<ergo_lib_ $collection_type_name:snake _new>](
+                collection_ptr_out: *mut [<$collection_type_name Ptr>],
+            ) {
+                #[allow(clippy::unwrap_used)]
+                ergo_lib_c_core::collections::collection_new(collection_ptr_out).unwrap();
+            }
+
+            /// Delete an existing collection
+            #[no_mangle]
+            pub unsafe extern "C" fn [<ergo_lib_ $collection_type_name:snake _delete>](ptr_out: [<$collection_type_name Ptr>]) {
+                ergo_lib_c_core::collections::collection_delete(ptr_out)
+            }
+
+            /// Returns length of an existing collection
+            #[no_mangle]
+            pub unsafe extern "C" fn [<ergo_lib_ $collection_type_name:snake _len>](
+                collection_ptr: [<Const $collection_type_name Ptr>],
+            ) -> usize {
+                #[allow(clippy::unwrap_used)]
+                ergo_lib_c_core::collections::collection_len(collection_ptr).unwrap()
+            }
+
+            /// Returns element at position `index` of an existing collection
+            #[no_mangle]
+            pub unsafe extern "C" fn [<ergo_lib_ $collection_type_name:snake _get>](
+                collection_ptr: [<Const $collection_type_name Ptr>],
+                index: usize,
+                element_ptr_out: *mut [<$item_type_name Ptr>],
+            ) -> crate::ReturnOption {
+                match ergo_lib_c_core::collections::collection_get(collection_ptr, index, element_ptr_out) {
+                    Ok(is_some) => crate::ReturnOption {
+                        is_some,
+                        error: std::ptr::null_mut(),
+                    },
+                    Err(e) => crate::ReturnOption {
+                        is_some: false, // Just a dummy value
+                        error: Error::c_api_from(Err(e)),
+                    },
+                }
+            }
+
+            #[no_mangle]
+            /// Add an element to collection
+            pub unsafe extern "C" fn [<ergo_lib_ $collection_type_name:snake _add>](
+                element_ptr: [<Const $item_type_name Ptr>],
+                collection_ptr_out: [<$collection_type_name Ptr>],
+            ) {
+                #[allow(clippy::unwrap_used)]
+                ergo_lib_c_core::collections::collection_add(collection_ptr_out, element_ptr).unwrap();
+            }
+
+        }
+    };
+}
+
+/// Generates an equality function for FFI
+#[macro_export]
+macro_rules! make_ffi_eq {
+    ($type_name:ident) => {
+        paste! {
+            #[no_mangle]
+            pub unsafe extern "C" fn [<ergo_lib_ $type_name:snake _eq>](
+              [< $type_name:snake _ptr_0>]: [< Const $type_name Ptr>],
+              [< $type_name:snake _ptr_1>]: [< Const $type_name Ptr>],
+            ) -> bool {
+              ergo_lib_c_core::util::deref_eq([< $type_name:snake _ptr_0>], [< $type_name:snake _ptr_1>])
+            }
+        }
+    };
+}
diff --git a/bindings/ergo-lib-c/src/reduced.rs b/bindings/ergo-lib-c/src/reduced.rs
new file mode 100644
index 000000000..ab1e3abc2
--- /dev/null
+++ b/bindings/ergo-lib-c/src/reduced.rs
@@ -0,0 +1,48 @@
+//! Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input
+//! is augmented with ReducedInput which contains a script reduction result.
+
+use crate::delete_ptr;
+use ergo_lib_c_core::{
+    collections::ConstCollectionPtr,
+    ergo_box::ErgoBox,
+    ergo_state_ctx::ConstErgoStateContextPtr,
+    reduced::*,
+    transaction::{ConstUnsignedTransactionPtr, UnsignedTransactionPtr},
+    Error, ErrorPtr,
+};
+
+/// Returns `reduced` transaction, i.e. unsigned transaction where each unsigned input
+/// is augmented with ReducedInput which contains a script reduction result.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_reduced_tx_from_unsigned_tx(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    state_context_ptr: ConstErgoStateContextPtr,
+    reduced_tx_out: *mut ReducedTransactionPtr,
+) -> ErrorPtr {
+    let res = reduced_tx_from_unsigned_tx(
+        unsigned_tx_ptr,
+        boxes_to_spend_ptr,
+        data_boxes_ptr,
+        state_context_ptr,
+        reduced_tx_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Returns the unsigned transation
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_reduced_tx_unsigned_tx(
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    unsigned_tx_out: *mut UnsignedTransactionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    reduced_tx_unsigned_tx(reduced_tx_ptr, unsigned_tx_out).unwrap();
+}
+
+/// Drop `ReducedTransaction`
+#[no_mangle]
+pub extern "C" fn ergo_lib_reduced_tx_delete(ptr: ReducedTransactionPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/secret_key.rs b/bindings/ergo-lib-c/src/secret_key.rs
new file mode 100644
index 000000000..04d4aea87
--- /dev/null
+++ b/bindings/ergo-lib-c/src/secret_key.rs
@@ -0,0 +1,50 @@
+use ergo_lib_c_core::{address::AddressPtr, secret_key::*, Error};
+use paste::paste;
+
+use crate::{delete_ptr, ErrorPtr};
+
+/// Parse dlog secret key from bytes (SEC-1-encoded scalar)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_secret_key_from_bytes(
+    bytes_ptr: *const u8,
+    secret_key_out: *mut SecretKeyPtr,
+) -> ErrorPtr {
+    let res = secret_key_from_bytes(bytes_ptr, secret_key_out);
+    Error::c_api_from(res)
+}
+
+/// Generate random key
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_secret_key_generate_random(secret_key_out: *mut SecretKeyPtr) {
+    #[allow(clippy::unwrap_used)]
+    secret_key_generate_random(secret_key_out).unwrap();
+}
+
+/// Address (encoded public image)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_secret_key_get_address(
+    secret_key_ptr: ConstSecretKeyPtr,
+    address_out: *mut AddressPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    secret_key_get_address(secret_key_ptr, address_out).unwrap();
+}
+
+/// Encode from a serialized key. Key assumption: 32 bytes must be allocated at the address
+/// pointed-to by `output`.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_secret_key_to_bytes(
+    secret_key_ptr: ConstSecretKeyPtr,
+    output: *mut u8,
+) {
+    #[allow(clippy::unwrap_used)]
+    secret_key_to_bytes(secret_key_ptr, output).unwrap();
+}
+
+/// Drop `SecretKey`
+#[no_mangle]
+pub extern "C" fn ergo_lib_secret_key_delete(ptr: SecretKeyPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_collection!(SecretKeys, SecretKey);
diff --git a/bindings/ergo-lib-c/src/token.rs b/bindings/ergo-lib-c/src/token.rs
new file mode 100644
index 000000000..2c0342c95
--- /dev/null
+++ b/bindings/ergo-lib-c/src/token.rs
@@ -0,0 +1,191 @@
+//! Token types
+use ergo_lib_c_core::{ergo_box::ConstBoxIdPtr, token::*, Error};
+use paste::paste;
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+};
+
+use crate::{delete_ptr, ErrorPtr, ReturnOption};
+
+// `TokenId` bindings ------------------------------------------------------------------------------
+
+/// Create token id from ergo box id (32 byte digest)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_id_from_box_id(
+    box_id_ptr: ConstBoxIdPtr,
+    token_id_out: *mut TokenIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    token_id_from_box_id(box_id_ptr, token_id_out).unwrap();
+}
+
+/// Parse token id (32 byte digest) from base16-encoded string
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_id_from_str(
+    bytes_ptr: *const c_char,
+    token_id_out: *mut TokenIdPtr,
+) -> ErrorPtr {
+    let str = CStr::from_ptr(bytes_ptr).to_string_lossy();
+    let res = token_id_from_str(&str, token_id_out);
+    Error::c_api_from(res)
+}
+
+/// Base16 encoded string
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_id_to_str(
+    token_id_ptr: ConstTokenIdPtr,
+    _str: *mut *const c_char,
+) {
+    #[allow(clippy::unwrap_used)]
+    {
+        let s = token_id_to_str(token_id_ptr).unwrap();
+        *_str = CString::new(s).unwrap().into_raw();
+    }
+}
+
+/// Drop `TokenId`
+#[no_mangle]
+pub extern "C" fn ergo_lib_token_id_delete(ptr: TokenIdPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(TokenId);
+
+// `TokenAmount` bindings --------------------------------------------------------------------------
+
+/// Create from i64 with bounds check
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_amount_from_i64(
+    amount: i64,
+    token_amount_out: *mut TokenAmountPtr,
+) -> ErrorPtr {
+    let res = token_amount_from_i64(amount, token_amount_out);
+    Error::c_api_from(res)
+}
+
+/// Get value as signed 64-bit long
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_amount_as_i64(
+    token_amount_ptr: ConstTokenAmountPtr,
+) -> i64 {
+    #[allow(clippy::unwrap_used)]
+    token_amount_as_i64(token_amount_ptr).unwrap()
+}
+
+/// Drop `TokenAmount`
+#[no_mangle]
+pub extern "C" fn ergo_lib_token_amount_delete(ptr: TokenAmountPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(TokenAmount);
+
+// `Token` bindings --------------------------------------------------------------------------------
+
+/// Create a token with given token id and amount
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_new(
+    token_id_ptr: ConstTokenIdPtr,
+    token_amount_ptr: ConstTokenAmountPtr,
+    token_out: *mut TokenPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    token_new(token_id_ptr, token_amount_ptr, token_out).unwrap();
+}
+
+/// Get token id
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_get_id(
+    token_ptr: ConstTokenPtr,
+    token_id_out: *mut TokenIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    token_get_id(token_ptr, token_id_out).unwrap();
+}
+
+/// Get token amount
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_get_amount(
+    token_ptr: ConstTokenPtr,
+    token_amount_out: *mut TokenAmountPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    token_get_amount(token_ptr, token_amount_out).unwrap();
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_token_to_json_eip12(
+    token_ptr: ConstTokenPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match token_to_json_eip12(token_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Drop `Token`
+#[no_mangle]
+pub extern "C" fn ergo_lib_token_delete(ptr: TokenPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(Token);
+
+// `Tokens` bindings -------------------------------------------------------------------------------
+
+/// Create an empty collection
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tokens_new(tokens_out: *mut TokensPtr) {
+    #[allow(clippy::unwrap_used)]
+    tokens_new(tokens_out).unwrap();
+}
+
+/// Returns length of collection
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tokens_len(tokens_ptr: ConstTokensPtr) -> usize {
+    #[allow(clippy::unwrap_used)]
+    tokens_len(tokens_ptr).unwrap()
+}
+
+/// If token at given index exists, allocate a copy and store in `token_out`.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tokens_get(
+    tokens_ptr: ConstTokensPtr,
+    index: usize,
+    token_out: *mut TokenPtr,
+) -> ReturnOption {
+    match tokens_get(tokens_ptr, index, token_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
+/// Add token to the end of the collection. There is a maximum capacity of 255 token, and adding
+/// more returns an error.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tokens_add(
+    token_ptr: ConstTokenPtr,
+    tokens_ptr: TokensPtr,
+) -> ErrorPtr {
+    Error::c_api_from(tokens_add(tokens_ptr, token_ptr))
+}
+
+/// Drop `Tokens`
+#[no_mangle]
+pub extern "C" fn ergo_lib_tokens_delete(ptr: TokensPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/transaction.rs b/bindings/ergo-lib-c/src/transaction.rs
new file mode 100644
index 000000000..0af5f6fcb
--- /dev/null
+++ b/bindings/ergo-lib-c/src/transaction.rs
@@ -0,0 +1,268 @@
+//! Ergo transaction
+
+use ergo_lib_c_core::{
+    collections::{CollectionPtr, ConstCollectionPtr},
+    data_input::DataInput,
+    ergo_box::{ErgoBox, ErgoBoxCandidate},
+    input::{Input, UnsignedInput},
+    transaction::*,
+    util::ByteArray,
+    Error, ErrorPtr,
+};
+
+use paste::paste;
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+};
+
+use crate::delete_ptr;
+
+// Need to define these here because the generated code from the `make_collection!` macro
+// invocations don't yet exist.
+type ConstByteArraysPtr = ConstCollectionPtr<ByteArray>;
+type DataInputsPtr = CollectionPtr<DataInput>;
+type InputsPtr = CollectionPtr<Input>;
+type UnsignedInputsPtr = CollectionPtr<UnsignedInput>;
+type ErgoBoxCandidatesPtr = CollectionPtr<ErgoBoxCandidate>;
+type ErgoBoxesPtr = CollectionPtr<ErgoBox>;
+
+// `UnsignedTransaction` bindings ------------------------------------------------------------------
+
+/// Get id for transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_id(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    tx_id_out: *mut TxIdPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    unsigned_tx_id(unsigned_tx_ptr, tx_id_out).unwrap();
+}
+
+/// Inputs for transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_inputs(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    unsigned_inputs_out: *mut UnsignedInputsPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    unsigned_tx_inputs(unsigned_tx_ptr, unsigned_inputs_out).unwrap();
+}
+
+/// Data inputs for transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_data_inputs(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    data_inputs_out: *mut DataInputsPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    unsigned_tx_data_inputs(unsigned_tx_ptr, data_inputs_out).unwrap();
+}
+
+/// Output candidates for transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_output_candidates(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    ergo_box_candidates_out: *mut ErgoBoxCandidatesPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    unsigned_tx_output_candidates(unsigned_tx_ptr, ergo_box_candidates_out).unwrap();
+}
+
+/// Parse from JSON. Supports Ergo Node/Explorer API and box values and token amount encoded as
+/// strings
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_from_json(
+    json_str: *const c_char,
+    unsigned_tx_out: *mut UnsignedTransactionPtr,
+) -> ErrorPtr {
+    let json = CStr::from_ptr(json_str).to_string_lossy();
+    let res = unsigned_tx_from_json(&json, unsigned_tx_out);
+    Error::c_api_from(res)
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_to_json(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match unsigned_tx_to_json(unsigned_tx_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_unsigned_tx_to_json_eip12(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match unsigned_tx_to_json_eip12(unsigned_tx_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Drop `UnsignedTransaction`
+#[no_mangle]
+pub extern "C" fn ergo_lib_unsigned_tx_delete(ptr: UnsignedTransactionPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+// `Transaction` bindings --------------------------------------------------------------------------
+
+/// Create Transaction from UnsignedTransaction and an array of proofs in the same order as
+/// UnsignedTransaction.inputs with empty proof indicated with empty byte array
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_from_unsigned_tx(
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    proofs_ptr: ConstByteArraysPtr,
+    tx_out: *mut TransactionPtr,
+) -> ErrorPtr {
+    let res = tx_from_unsigned_tx(unsigned_tx_ptr, proofs_ptr, tx_out);
+    Error::c_api_from(res)
+}
+
+/// Get id for transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_id(tx_ptr: ConstTransactionPtr, tx_id_out: *mut TxIdPtr) {
+    #[allow(clippy::unwrap_used)]
+    tx_id(tx_ptr, tx_id_out).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_inputs(
+    tx_ptr: ConstTransactionPtr,
+    inputs_out: *mut InputsPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_inputs(tx_ptr, inputs_out).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_data_inputs(
+    tx_ptr: ConstTransactionPtr,
+    data_inputs_out: *mut DataInputsPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_data_inputs(tx_ptr, data_inputs_out).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_output_candidates(
+    tx_ptr: ConstTransactionPtr,
+    ergo_box_candidates_out: *mut ErgoBoxCandidatesPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_output_candidates(tx_ptr, ergo_box_candidates_out).unwrap();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_outputs(
+    tx_ptr: ConstTransactionPtr,
+    ergo_box_out: *mut ErgoBoxesPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_outputs(tx_ptr, ergo_box_out).unwrap();
+}
+
+/// Parse from JSON. Supports Ergo Node/Explorer API and box values and token amount encoded as
+/// strings
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_from_json(
+    json_str: *const c_char,
+    tx_out: *mut TransactionPtr,
+) -> ErrorPtr {
+    let json = CStr::from_ptr(json_str).to_string_lossy();
+    let res = tx_from_json(&json, tx_out);
+    Error::c_api_from(res)
+}
+
+/// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_to_json(
+    tx_ptr: ConstTransactionPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match tx_to_json(tx_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_to_json_eip12(
+    tx_ptr: ConstTransactionPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match tx_to_json_eip12(tx_ptr) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Drop `Transaction`
+#[no_mangle]
+pub extern "C" fn ergo_lib_tx_delete(ptr: TransactionPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+// `TxId` bindings ----------------------------------------------------------------------
+
+/// Convert a hex string into a TxId
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_id_from_str(
+    str: *const c_char,
+    tx_id_out: *mut TxIdPtr,
+) -> ErrorPtr {
+    let str = CStr::from_ptr(str).to_string_lossy();
+    let res = tx_id_from_str(&str, tx_id_out);
+    Error::c_api_from(res)
+}
+
+/// Get the tx id as bytes
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_id_to_str(
+    tx_id_ptr: ConstTxIdPtr,
+    _str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match tx_id_to_str(tx_id_ptr) {
+        Ok(s) => {
+            *_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+/// Drop `TxId`
+#[no_mangle]
+pub extern "C" fn ergo_lib_tx_id_delete(ptr: TxIdPtr) {
+    unsafe { delete_ptr(ptr) }
+}
+
+make_ffi_eq!(TxId);
diff --git a/bindings/ergo-lib-c/src/tx_builder.rs b/bindings/ergo-lib-c/src/tx_builder.rs
new file mode 100644
index 000000000..6346ad73c
--- /dev/null
+++ b/bindings/ergo-lib-c/src/tx_builder.rs
@@ -0,0 +1,147 @@
+//! Unsigned transaction builder
+
+use ergo_lib_c_core::{
+    address::{AddressPtr, ConstAddressPtr},
+    box_selector::{BoxSelectionPtr, ConstBoxSelectionPtr},
+    collections::{CollectionPtr, ConstCollectionPtr},
+    data_input::DataInput,
+    ergo_box::{BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidate},
+    transaction::UnsignedTransactionPtr,
+    tx_builder::*,
+    Error, ErrorPtr,
+};
+
+use crate::delete_ptr;
+
+/// Suggested transaction fee (semi-default value used across wallets and dApps as of Oct 2020)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_suggested_tx_fee(value_out: *mut BoxValuePtr) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_suggested_tx_fee(value_out).unwrap();
+}
+
+/// Creates new TxBuilder
+/// `box_selection` - selected input boxes (via [`super::box_selector`])
+/// `output_candidates` - output boxes to be "created" in this transaction,
+/// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
+/// `fee_amount` - miner's fee,
+/// `change_address` - change (inputs - outputs) will be sent to this address,
+/// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
+/// will be given to miners,
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_new(
+    box_selection_ptr: ConstBoxSelectionPtr,
+    output_candidates_ptr: ConstCollectionPtr<ErgoBoxCandidate>,
+    current_height: u32,
+    fee_amount_ptr: ConstBoxValuePtr,
+    change_address_ptr: ConstAddressPtr,
+    min_change_value_ptr: ConstBoxValuePtr,
+    tx_builder_out: *mut TxBuilderPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_new(
+        box_selection_ptr,
+        output_candidates_ptr,
+        current_height,
+        fee_amount_ptr,
+        change_address_ptr,
+        min_change_value_ptr,
+        tx_builder_out,
+    )
+    .unwrap();
+}
+
+/// Set transaction's data inputs
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_set_data_inputs(
+    tx_builder_mut: TxBuilderPtr,
+    data_inputs_ptr: ConstCollectionPtr<DataInput>,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_set_data_inputs(tx_builder_mut, data_inputs_ptr).unwrap();
+}
+
+/// Build the unsigned transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_build(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    unsigned_transaction_out: *mut UnsignedTransactionPtr,
+) -> ErrorPtr {
+    let res = tx_builder_build(tx_builder_ptr, unsigned_transaction_out);
+    Error::c_api_from(res)
+}
+
+/// Get box selection
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_box_selection(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    box_selection_out: *mut BoxSelectionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_box_selection(tx_builder_ptr, box_selection_out).unwrap();
+}
+
+/// Get data inputs
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_data_inputs(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    data_inputs_out: *mut CollectionPtr<DataInput>,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_data_inputs(tx_builder_ptr, data_inputs_out).unwrap();
+}
+
+/// Get outputs EXCLUDING fee and change
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_output_candidates(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    output_candidates_out: *mut CollectionPtr<ErgoBoxCandidate>,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_output_candidates(tx_builder_ptr, output_candidates_out).unwrap();
+}
+
+/// Get current height
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_current_height(
+    tx_builder_ptr: ConstTxBuilderPtr,
+) -> u32 {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_current_height(tx_builder_ptr).unwrap()
+}
+
+/// Get fee amount
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_fee_amount(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    value_out: *mut BoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_fee_amount(tx_builder_ptr, value_out).unwrap();
+}
+
+/// Get change address
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_change_address(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    address_out: *mut AddressPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_change_address(tx_builder_ptr, address_out).unwrap();
+}
+
+/// Get min change value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_tx_builder_min_change_value(
+    tx_builder_ptr: ConstTxBuilderPtr,
+    min_change_value_out: *mut BoxValuePtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    tx_builder_min_change_value(tx_builder_ptr, min_change_value_out).unwrap();
+}
+
+/// Drop `TxBuilder`
+#[no_mangle]
+pub extern "C" fn ergo_lib_tx_builder_delete(ptr: TxBuilderPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-c/src/wallet.rs b/bindings/ergo-lib-c/src/wallet.rs
new file mode 100644
index 000000000..ae749f77f
--- /dev/null
+++ b/bindings/ergo-lib-c/src/wallet.rs
@@ -0,0 +1,78 @@
+//! Wallet-like features
+
+use std::{ffi::CStr, os::raw::c_char};
+
+use ergo_lib_c_core::{
+    collections::ConstCollectionPtr,
+    ergo_box::ErgoBox,
+    ergo_state_ctx::ConstErgoStateContextPtr,
+    reduced::ConstReducedTransactionPtr,
+    secret_key::SecretKey,
+    transaction::{ConstUnsignedTransactionPtr, TransactionPtr},
+    wallet::*,
+    Error, ErrorPtr,
+};
+
+use crate::delete_ptr;
+
+/// Create `Wallet` instance loading secret key from mnemonic
+/// Returns Err if a DlogSecretKey cannot be parsed from the provided phrase
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_from_mnemonic(
+    mnemonic_phrase: *const c_char,
+    mnemonic_pass: *const c_char,
+    wallet_out: *mut WalletPtr,
+) -> ErrorPtr {
+    let mnemonic_phrase = CStr::from_ptr(mnemonic_phrase).to_string_lossy();
+    let mnemonic_pass = CStr::from_ptr(mnemonic_pass).to_string_lossy();
+    let res = wallet_from_mnemonic(&mnemonic_phrase, &mnemonic_pass, wallet_out);
+    Error::c_api_from(res)
+}
+
+/// Create `Wallet` from secrets
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_from_secrets(
+    secret_keys_ptr: ConstCollectionPtr<SecretKey>,
+    wallet_out: *mut WalletPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    wallet_from_secrets(secret_keys_ptr, wallet_out).unwrap();
+}
+
+/// Signs a transaction
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_sign_transaction(
+    wallet_ptr: ConstWalletPtr,
+    state_context_ptr: ConstErgoStateContextPtr,
+    unsigned_tx_ptr: ConstUnsignedTransactionPtr,
+    boxes_to_spend_ptr: ConstCollectionPtr<ErgoBox>,
+    data_boxes_ptr: ConstCollectionPtr<ErgoBox>,
+    transaction_out: *mut TransactionPtr,
+) -> ErrorPtr {
+    let res = wallet_sign_transaction(
+        wallet_ptr,
+        state_context_ptr,
+        unsigned_tx_ptr,
+        boxes_to_spend_ptr,
+        data_boxes_ptr,
+        transaction_out,
+    );
+    Error::c_api_from(res)
+}
+
+/// Signs a reduced transaction (generating proofs for inputs)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_wallet_sign_reduced_transaction(
+    wallet_ptr: ConstWalletPtr,
+    reduced_tx_ptr: ConstReducedTransactionPtr,
+    transaction_out: *mut TransactionPtr,
+) -> ErrorPtr {
+    let res = wallet_sign_reduced_transaction(wallet_ptr, reduced_tx_ptr, transaction_out);
+    Error::c_api_from(res)
+}
+
+/// Drop `Wallet`
+#[no_mangle]
+pub extern "C" fn ergo_lib_wallet_delete(ptr: WalletPtr) {
+    unsafe { delete_ptr(ptr) }
+}
diff --git a/bindings/ergo-lib-ios/Config/Release.xcconfig b/bindings/ergo-lib-ios/Config/iPhoneOS.xcconfig
similarity index 88%
rename from bindings/ergo-lib-ios/Config/Release.xcconfig
rename to bindings/ergo-lib-ios/Config/iPhoneOS.xcconfig
index 006d517ca..705d4411f 100644
--- a/bindings/ergo-lib-ios/Config/Release.xcconfig
+++ b/bindings/ergo-lib-ios/Config/iPhoneOS.xcconfig
@@ -1,6 +1,5 @@
 CONFIGURATION = Release
 TARGET_NAME = ErgoLib
-SUPPORTED_PLATFORMS = iphonesimulator iphoneos
 VALID_ARCHS = arm64 x86_64
 ENABLE_BITCODE = NO
 BUILD_LIBRARY_FOR_DISTRIBUTION = YES
diff --git a/bindings/ergo-lib-ios/Config/iPhoneSimulator_intel.xcconfig b/bindings/ergo-lib-ios/Config/iPhoneSimulator_intel.xcconfig
new file mode 100644
index 000000000..e6bc12067
--- /dev/null
+++ b/bindings/ergo-lib-ios/Config/iPhoneSimulator_intel.xcconfig
@@ -0,0 +1,11 @@
+CONFIGURATION = Release
+TARGET_NAME = ErgoLib
+EXCLUDED_ARCHS = armv7 arm64 i386
+ENABLE_BITCODE = NO
+BUILD_LIBRARY_FOR_DISTRIBUTION = YES
+HEADER_SEARCH_PATHS = $(inherited) $(SRCROOT)/../ergo-lib-c/h
+OTHER_LDFLAGS = $(inherited) -L"$(SRCROOT)/../../target/release" -lergo_lib_c
+PRODUCT_BUNDLE_IDENTIFIER = org.ergoplatform.wallet
+FRAMEWORK_VERSION = 0.1.0-pre1
+
+
diff --git a/bindings/ergo-lib-ios/Package.swift b/bindings/ergo-lib-ios/Package.swift
index 2f6a335b1..f53bcc1f4 100644
--- a/bindings/ergo-lib-ios/Package.swift
+++ b/bindings/ergo-lib-ios/Package.swift
@@ -16,5 +16,6 @@ let package = Package(
             name: "ErgoLib",
             dependencies: ["ErgoLibC"]
         ),
+        .testTarget(name: "ErgoLibTests", dependencies: ["ErgoLib"])
     ]
 )
diff --git a/bindings/ergo-lib-ios/README.md b/bindings/ergo-lib-ios/README.md
index 5b23eb7d3..75d50af51 100644
--- a/bindings/ergo-lib-ios/README.md
+++ b/bindings/ergo-lib-ios/README.md
@@ -1,1 +1,75 @@
-## Swift wrapper for [C bindings](../ergo-lib-c) of ergo-lib.
+# Swift wrapper for [C bindings](../ergo-lib-c) of ergo-lib.
+
+
+## Build instructions
+
+First build `ergo-lib-c`
+```
+cargo build --release -p ergo-lib-c
+```
+
+This creates a static library under `<project_root_directory>/target/release`. Next we use `cbindgen`
+to generate C-headers.
+**Important:** we need a nightly version of `rustc` for this step, as we use macros to generate
+*portions of the C-bindings. `cbindgen` can't 'see' through the macros so the nightly compiler is
+used to expand the macros before `cbindgen` passes over the code.
+
+```shell
+cd bindings/ergo-lib-c
+rustup override set nightly
+cbindgen --config cbindgen.toml --crate ergo-lib-c --output h/ergo_lib.h
+rustup override set stable
+```
+
+
+To build this project we need to point `swift` to this directory for linking.
+```
+cd ../ergo-lib-ios
+swift build -Xlinker -L../../target/release/
+```
+
+To run tests we must also pass in the library directory:
+```
+swift test -Xlinker -L../../target/release/
+```
+ 
+
+### Building Xcode 13 project for `iPhoneSimulator` (for Intel macs)
+
+Make sure `ergo-lib-c` is built as described above.
+
+Starting at the root of the `sigma-rust` repository, type:
+
+```shell
+cd bindings/ergo-lib-ios
+swift package generate-xcodeproj
+xcodebuild -project ./ErgoLib.xcodeproj -xcconfig ./Config/iPhoneSimulator_intel.xcconfig -sdk iphoneos
+```
+
+Then double click `ErgoLib.xcodeproj` which opens Xcode. You need to manually point the linker towards the directory where the `ergo-lib` static library resides. Refer to the following image: 
+
+![image](xcode_linker_settings.png)
+
+Set `Other Linker Flags` to be `-L/absolute/path/to/sigma-rust/target/release`. The project can now be built.
+
+### Building Xcode 13 project for `iPhone(iOS)`
+
+First we need to build an ARM64 target for `ergo-lib-c`: 
+
+```shell
+rustup target add aarch64-apple-ios
+cargo build --release --target=aarch64-apple-ios -p ergo-lib-c
+```
+
+Next starting at the root of the `sigma-rust` repository, type:
+
+```shell
+cd bindings/ergo-lib-ios
+swift package generate-xcodeproj
+xcodebuild -project ./ErgoLib.xcodeproj -xcconfig ./Config/iPhoneOS.xcconfig -sdk iphoneos
+```
+
+Next navigate in Xcode to `Build Settings` as indicated by 1 and 2 in the picture below. Set the fields `Base SDK`, `Excluded Architecture` and `Supported Platforms` **exactly** as pictured by 3 and 4.
+![image](xcode_ios_settings.png)
+
+Then set `Other Linker Flags` to be `-L/absolute/path/to/sigma-rust/aarch64-apple-ios/release`. Finally we can build the project.
\ No newline at end of file
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Address.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Address.swift
new file mode 100644
index 000000000..4ee5e0fad
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Address.swift
@@ -0,0 +1,120 @@
+import Foundation
+import ErgoLibC
+
+/**
+ * An address is a short string corresponding to some script used to protect a box. Unlike (string-encoded) binary
+ * representation of a script, an address has some useful characteristics:
+ *
+ * - Integrity of an address could be checked., as it is incorporating a checksum.
+ * - A prefix of address is showing network and an address type.
+ * - An address is using an encoding (namely, Base58) which is avoiding similarly l0Oking characters, friendly to
+ * double-clicking and line-breaking in emails.
+ *
+ *
+ *
+ * An address is encoding network type, address type, checksum, and enough information to watch for a particular scripts.
+ *
+ * Possible network types are:
+ * Mainnet - 0x00
+ * Testnet - 0x10
+ *
+ * For an address type, we form content bytes as follows:
+ *
+ * P2PK - serialized (compressed) public key
+ * P2SH - first 192 bits of the Blake2b256 hash of serialized script bytes
+ * P2S  - serialized script
+ *
+ * Address examples for testnet:
+ *
+ * 3   - P2PK (3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN)
+ * ?   - P2SH (rbcrmKEYduUvADj9Ts3dSVSG27h54pgrq5fPuwB)
+ * ?   - P2S (Ms7smJwLGbUAjuWQ)
+ *
+ * for mainnet:
+ *
+ * 9  - P2PK (9fRAWhdxEsTcdb8PhGNrZfwqa65zfkuYHAMmkQLcic1gdLSV5vA)
+ * ?  - P2SH (8UApt8czfFVuTgQmMwtsRBZ4nfWquNiSwCWUjMg)
+ * ?  - P2S (4MQyML64GnzMxZgm, BxKBaHkvrTvLZrDcZjcsxsF7aSsrN73ijeFZXtbj4CXZHHcvBtqSxQ)
+ *
+ *
+ * Prefix byte = network type + address type
+ *
+ * checksum = blake2b256(prefix byte ++ content bytes)
+ *
+ * address = prefix byte ++ content bytes ++ checksum
+ *
+ */
+class Address {
+    internal var pointer: AddressPtr
+
+    /// Decode (base58) testnet address from string, checking that address is from the testnet
+    init(withTestnetAddress addressStr: String) throws {
+        var ptr: AddressPtr?
+        let error = addressStr.withCString { cs in
+            ergo_lib_address_from_testnet(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+
+    /// Decode (base58) mainnet address from string, checking that address is from the mainnet
+    init(withMainnetAddress addressStr: String) throws {
+        var ptr: AddressPtr?
+        let error = addressStr.withCString { cs in
+            ergo_lib_address_from_mainnet(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Decode (base58) address from string without checking the network prefix
+    init(withBase58Address addressStr: String) throws {
+        var ptr: AddressPtr?
+        let error = addressStr.withCString { cs in
+            ergo_lib_address_from_base58(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``AddressPtr``. Note: we must ensure that no other instance
+    /// of ``Address`` can hold this pointer.
+    internal init(withRawPointer ptr: AddressPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Encode (base58) address
+    func toBase58(networkPrefix: NetworkPrefix) -> String {
+        var cStr: UnsafePointer<CChar>?
+        ergo_lib_address_to_base58(self.pointer, networkPrefix.rawValue, &cStr)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return str
+    }
+    
+    /// Get the type of the address
+    func typePrefix() -> AddressTypePrefix {
+        let value = ergo_lib_address_type_prefix(self.pointer)
+        return AddressTypePrefix(rawValue: value)!
+    }
+    
+    deinit {
+        ergo_lib_address_delete(self.pointer)
+    }
+}
+
+/// Network type
+enum NetworkPrefix: UInt8 {
+    case Mainnet = 0
+    case Testnet = 16
+}
+
+/// Address types
+enum AddressTypePrefix: UInt8 {
+    /// 0x01 - Pay-to-PublicKey(P2PK) address
+    case P2Pk = 1
+    /// 0x02 - Pay-to-Script-Hash(P2SH)
+    case Pay2Sh = 2
+    /// 0x03 - Pay-to-Script(P2S)
+    case Pay2S = 3
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
new file mode 100644
index 000000000..195444609
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
@@ -0,0 +1,96 @@
+import Foundation
+import ErgoLibC
+
+/// Represents data of the block header available in Sigma propositions.
+class BlockHeader {
+    internal var pointer: BlockHeaderPtr
+    
+    /// Parse BlockHeader array from JSON (Node API)
+    init(withJson json: String) throws {
+        var ptr: BlockHeaderPtr?
+        let error = json.withCString { cs in
+            ergo_lib_block_header_from_json(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``BlockHeaderPtr``. Note: we must ensure that no other instance
+    /// of ``BlockHeader`` can hold this pointer.
+    internal init(withRawPointer ptr: BlockHeaderPtr) {
+        self.pointer = ptr
+    }
+    
+    deinit {
+        ergo_lib_block_header_delete(self.pointer)
+    }
+}
+
+extension BlockHeader: Equatable {
+    static func ==(lhs: BlockHeader, rhs: BlockHeader) -> Bool {
+        ergo_lib_block_header_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// An ordered collection of ``BlockHeader``s
+class BlockHeaders {
+    internal var pointer: BlockHeadersPtr
+    
+    /// Create an empty collection
+    init() {
+        self.pointer = BlockHeaders.initRawPtrEmpty()
+    }
+    /// Parse ``BlockHeader`` array from JSON (Node API)
+    init(fromJSON: Any) throws {
+        let json = JSON(fromJSON)
+        if let arr = json.array {
+            let headers = try arr.map{ elem -> BlockHeader in
+                if let jsonStr = elem.rawString() {
+                    return try BlockHeader(withJson: jsonStr);
+                } else {
+                    throw WalletError.walletCError(reason: "BlockHeaders.init(fromJSON): cannot cast array element to raw JSON string")
+                }
+            }
+            self.pointer = BlockHeaders.initRawPtrEmpty()
+            for header in headers {
+                self.add(blockHeader: header)
+            }
+        } else {
+            throw WalletError.walletCError(reason: "BlockHeaders.init(fromJSON): expected [JSON]")
+        }
+    }
+    
+    /// Use the C-API to create an empty collection and return the raw pointer that points to this
+    /// collection.
+    private static func initRawPtrEmpty() -> BlockHeaderPtr {
+        var ptr: BlockHeadersPtr?
+        ergo_lib_block_headers_new(&ptr)
+        return ptr!
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_block_headers_len(self.pointer)
+    }
+    
+    /// Returns the ``BlockHeader`` at location `index` if it exists.
+    func get(index: UInt) -> BlockHeader? {
+        var blockHeaderPtr: BlockHeaderPtr?
+        let res = ergo_lib_block_headers_get(self.pointer, index, &blockHeaderPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return BlockHeader(withRawPointer: blockHeaderPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``BlockHeader`` to the end of the collection.
+    func add(blockHeader: BlockHeader) {
+        ergo_lib_block_headers_add(blockHeader.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_block_headers_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/BoxSelector.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/BoxSelector.swift
new file mode 100644
index 000000000..77163e633
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/BoxSelector.swift
@@ -0,0 +1,85 @@
+import Foundation
+import ErgoLibC
+
+/// Selected boxes with change boxes. Instances of this class are created by ``BoxSelector``.
+class BoxSelection {
+    internal var pointer: BoxSelectionPtr
+    
+    /// Create a selection to easily inject custom selection algorithms
+    init(ergoBoxes: ErgoBoxes, changeErgoBoxes: ErgoBoxAssetsDataList) {
+        var ptr: BoxSelectionPtr?
+        ergo_lib_box_selection_new(ergoBoxes.pointer, changeErgoBoxes.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``BoxSelectionPtr``. Note: we must ensure that no other instance
+    /// of ``BoxSelection`` can hold this pointer.
+    internal init(withRawPointer ptr: BoxIdPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Selected boxes to spend as transaction inputs
+    func getBoxes() -> ErgoBoxes {
+        var ptr: ErgoBoxesPtr?
+        ergo_lib_box_selection_boxes(self.pointer, &ptr)
+        return ErgoBoxes(withRawPointer: ptr!)
+    }
+    
+    /// Selected boxes to use as change
+    func getChangeBoxes() -> ErgoBoxAssetsDataList {
+        var ptr: ErgoBoxAssetsDataListPtr?
+        ergo_lib_box_selection_change(self.pointer, &ptr)
+        return ErgoBoxAssetsDataList(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_box_selection_delete(self.pointer)
+    }
+}
+
+extension BoxSelection: Equatable {
+    static func ==(lhs: BoxSelection, rhs: BoxSelection) -> Bool {
+        ergo_lib_box_selection_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Naive box selector, collects inputs until target balance is reached
+class SimpleBoxSelector {
+    internal var pointer: SimpleBoxSelectorPtr
+    
+    init() {
+        var ptr: SimpleBoxSelectorPtr?
+        ergo_lib_simple_box_selector_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    internal init(withPtr ptr: BoxIdPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Selects inputs to satisfy target balance and tokens.
+    /// - Parameters:
+    ///  - `inputs`: available inputs (returns an error, if empty),
+    ///  - `targetBalance: coins (in nanoERGs) needed,
+    ///  - `targetTokens: amount of tokens needed.
+    /// - Returns: selected inputs and box assets(value+tokens) with change.
+    func select( inputs: ErgoBoxes,
+          targetBalance: BoxValue,
+          targetTokens: Tokens
+    ) throws -> BoxSelection {
+        var ptr: BoxSelectionPtr?
+        let error = ergo_lib_simple_box_selector_select(
+            self.pointer,
+            inputs.pointer,
+            targetBalance.pointer,
+            targetTokens.pointer,
+            &ptr
+        )
+        try checkError(error)
+        return BoxSelection(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_simple_box_selector_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ByteArray.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ByteArray.swift
new file mode 100644
index 000000000..8a66cf060
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ByteArray.swift
@@ -0,0 +1,56 @@
+import Foundation
+import ErgoLibC
+
+class ByteArray {
+    internal var pointer: ByteArrayPtr
+    
+    init(fromBytes : [UInt8]) throws {
+        var ptr: ByteArrayPtr?
+        let error = ergo_lib_byte_array_from_raw_parts(fromBytes, UInt(fromBytes.count), &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ByteArrayPtr``. Note: we must ensure that no other instance
+    /// of ``ByteArray`` can hold this pointer.
+    internal init(withRawPointer ptr: ByteArrayPtr) {
+        self.pointer = ptr
+    }
+    
+    deinit {
+        ergo_lib_byte_array_delete(self.pointer)
+    }
+}
+
+class ByteArrays {
+    internal var pointer: ByteArraysPtr
+    
+    init() {
+        var ptr: ByteArraysPtr?
+        ergo_lib_byte_arrays_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    func len() -> UInt {
+        return ergo_lib_byte_arrays_len(self.pointer)
+    }
+    
+    func get(index: UInt) -> ByteArray? {
+        var ptr: ByteArrayPtr?
+        let res = ergo_lib_byte_arrays_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return ByteArray(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    func add(byteArray: ByteArray) {
+        ergo_lib_byte_arrays_add(byteArray.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_byte_arrays_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Constant.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Constant.swift
new file mode 100644
index 000000000..d3ae0958c
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Constant.swift
@@ -0,0 +1,115 @@
+
+import Foundation
+import ErgoLibC
+
+/// Ergo constant(evaluated) values
+class Constant {
+    internal var pointer: ConstantPtr
+    
+    /// Decode from Base16-encoded ErgoTree serialized value
+    init(withBase16Str: String) throws  {
+        var ptr: ConstantPtr?
+        let error = withBase16Str.withCString { cs in
+            ergo_lib_constant_from_base16(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Create from ``Int32`` value
+    init(withInt32: Int32) {
+        var ptr: ConstantPtr?
+        ergo_lib_constant_from_i32(withInt32, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Create from ``Int64`` value
+    init(withInt64: Int64) {
+        var ptr: ConstantPtr?
+        ergo_lib_constant_from_i64(withInt64, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Create from byte array
+    init(withBytes: [UInt8]) throws {
+        var ptr: ConstantPtr?
+        let error = ergo_lib_constant_from_bytes(withBytes, UInt(withBytes.count), &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Parse raw [`EcPoint`] value from bytes and make [`ProveDlog`] constant
+    init(withECPointBytes: [UInt8]) throws {
+        var ptr: ConstantPtr?
+        let error = ergo_lib_constant_from_ecpoint_bytes(withECPointBytes, UInt(withECPointBytes.count), &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Create from ``ErgoBox`` value
+    init(withErgoBox: ErgoBox) {
+        var ptr: ConstantPtr?
+        ergo_lib_constant_from_ergo_box(withErgoBox.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ConstantPtr``. Note: we must ensure that no other instance
+    /// of ``Constant`` can hold this pointer.
+    internal init(withRawPointer ptr: ConstantPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Encode as Base16-encoded ErgoTree serialized value or throw an error if serialization
+    /// failed
+    func toBase16String() throws -> String {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_constant_to_base16(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return str
+    }
+    
+    /// Extract ``Int32`` value, throwing error if wrong type
+    func toInt32() throws -> Int32 {
+        let res = ergo_lib_constant_to_i32(self.pointer)
+        try checkError(res.error)
+        return res.value
+    }
+    
+    /// Extract ``Int64`` value, throwing error if wrong type
+    func toInt64() throws -> Int64 {
+        let res = ergo_lib_constant_to_i64(self.pointer)
+        try checkError(res.error)
+        return res.value
+    }
+    
+    /// Extract  byte array ``[UInt8]`` value, throwing error if wrong type
+    func toBytes() throws -> [UInt8] {
+        let res = ergo_lib_constant_bytes_len(self.pointer)
+        try checkError(res.error)
+        var bytes = Array.init(repeating: UInt8(0), count: Int(res.value))
+        let error = ergo_lib_constant_to_bytes(self.pointer, &bytes)
+        try checkError(error)
+        return bytes
+    }
+    
+    /// Extract ``ErgoBox`` value, throwing error if wrong type
+    func toErgoBox() throws -> ErgoBox {
+        var ptr: ErgoBoxPtr?
+        let error = ergo_lib_constant_to_ergo_box(self.pointer, &ptr)
+        try checkError(error)
+        return ErgoBox(withRawPointer: ptr!)
+    }
+    
+    
+    deinit {
+        ergo_lib_constant_delete(self.pointer)
+    }
+}
+
+extension Constant: Equatable {
+    static func ==(lhs: Constant, rhs: Constant) -> Bool {
+        ergo_lib_constant_eq(lhs.pointer, rhs.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ContextExtension.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ContextExtension.swift
new file mode 100644
index 000000000..978d845ad
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ContextExtension.swift
@@ -0,0 +1,33 @@
+
+import Foundation
+import ErgoLibC
+
+/// User-defined variables to be put into context
+class ContextExtension {
+    internal var pointer: ContextExtensionPtr
+    
+    /// Returns an empty ``ContextExtension``
+    init() {
+        var ptr: ContextExtensionPtr?
+        ergo_lib_context_extension_empty(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Returns all keys (``UInt8`` values) in the map
+    func getKeys() -> [UInt8] {
+        let bytesLength = ergo_lib_context_extension_len(self.pointer)
+        var bytes = Array.init(repeating: UInt8(0), count: Int(bytesLength))
+        ergo_lib_context_extension_keys(self.pointer, &bytes)
+        return bytes
+    }
+    
+    /// Takes ownership of an existing ``ContextExtensionPtr``. Note: we must ensure that no other instance
+    /// of ``ContextExtension`` can hold this pointer.
+    internal init(withPtr ptr: ContextExtensionPtr) {
+        self.pointer = ptr
+    }
+    
+    deinit {
+        ergo_lib_context_extension_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Contract.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Contract.swift
new file mode 100644
index 000000000..7726809dc
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Contract.swift
@@ -0,0 +1,53 @@
+import Foundation
+import ErgoLibC
+
+/// Defines the contract(script) that will be guarding box contents
+class Contract {
+    internal var pointer: ContractPtr
+    
+    internal init(withRawPtr ptr: ContractPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Create new contract from ErgoTree
+    init(fromErgoTree: ErgoTree) {
+        var ptr:  ContractPtr?
+        ergo_lib_contract_new(fromErgoTree.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Compiles a contract from ErgoScript source code
+    init(compileFromString: String) throws {
+        var ptr: ContractPtr?
+        let error = compileFromString.withCString { cs in
+            ergo_lib_contract_compile(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Create new contract that allow spending of the guarded box by a given recipient ([`Address`])
+    init(payToAddress: Address) throws {
+        var ptr: ContractPtr?
+        let error = ergo_lib_contract_pay_to_address(payToAddress.pointer, &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Get the ErgoTree of the contract
+    func getErgoTree() -> ErgoTree {
+        var boxIdPtr: ErgoTreePtr?
+        ergo_lib_contract_ergo_tree(self.pointer, &boxIdPtr)
+        return ErgoTree(withRawPointer: boxIdPtr!)
+    }
+        
+    deinit {
+        ergo_lib_contract_delete(self.pointer)
+    }
+}
+
+extension Contract: Equatable {
+    static func ==(lhs: Contract, rhs: Contract) -> Bool {
+        ergo_lib_contract_eq(lhs.pointer, rhs.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/DataInput.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/DataInput.swift
new file mode 100644
index 000000000..77c3fc9b8
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/DataInput.swift
@@ -0,0 +1,81 @@
+import Foundation
+import ErgoLibC
+
+/// Inputs, that are used to enrich script context, but won't be spent by the transaction
+class DataInput {
+    internal var pointer: DataInputPtr
+    
+    /// Parse box id (32 byte digest)
+    init(withBoxId: BoxId) {
+        var ptr: DataInputPtr?
+        ergo_lib_data_input_new(withBoxId.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``DataInputPtr``. Note: we must ensure that no other instance
+    /// of ``DataInput`` can hold this pointer.
+    internal init(withRawPointer ptr: DataInputPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get box id
+    func getBoxId() -> BoxId {
+        var boxIdPtr: BoxIdPtr?
+        ergo_lib_data_input_box_id(self.pointer, &boxIdPtr)
+        return BoxId(withRawPointer: boxIdPtr!)
+    }
+        
+    deinit {
+        ergo_lib_data_input_delete(self.pointer)
+    }
+}
+
+/// An ordered collection of `BlockHeader`s
+class DataInputs {
+    internal var pointer: DataInputsPtr
+    
+    /// Create an empty collection
+    init() {
+        self.pointer = DataInputs.initRawPtrEmpty()
+    }
+    
+    /// Takes ownership of an existing ``DataInputsPtr``. Note: we must ensure that no other instance
+    /// of ``DataInputs`` can hold this pointer.
+    init(withRawPointer ptr: DataInputsPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Use the C-API to create an empty collection and return the raw pointer that points to this
+    /// collection.
+    private static func initRawPtrEmpty() -> DataInputsPtr {
+        var dataInputsPtr: DataInputsPtr?
+        ergo_lib_data_inputs_new(&dataInputsPtr)
+        return dataInputsPtr!
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_data_inputs_len(self.pointer)
+    }
+    
+    /// Returns the ``DataInput`` at location `index` if it exists.
+    func get(index: UInt) -> DataInput? {
+        var ptr: DataInputPtr?
+        let res = ergo_lib_data_inputs_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return DataInput(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``DataInput`` to the end of the collection.
+    func add(dataInput: DataInput) {
+        ergo_lib_data_inputs_add(dataInput.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_data_inputs_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoBox.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoBox.swift
new file mode 100644
index 000000000..736f4d405
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoBox.swift
@@ -0,0 +1,532 @@
+/// Box (aka coin, or an unspent output) is a basic concept of a UTXO-based cryptocurrency.
+/// In Bitcoin, such an object is associated with some monetary value (arbitrary,
+/// but with predefined precision, so we use integer arithmetic to work with the value),
+/// and also a guarding script (aka proposition) to protect the box from unauthorized opening.
+///
+/// In other way, a box is a state element locked by some proposition (ErgoTree).
+///
+/// In Ergo, box is just a collection of registers, some with mandatory types and semantics,
+/// others could be used by applications in any way.
+/// We add additional fields in addition to amount and proposition~(which stored in the registers R0 and R1).
+/// Namely, register R2 contains additional tokens (a sequence of pairs (token identifier, value)).
+/// Register R3 contains height when block got included into the blockchain and also transaction
+/// identifier and box index in the transaction outputs.
+/// Registers R4-R9 are free for arbitrary usage.
+///
+/// A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
+//! can not be linked to the same box.
+import Foundation
+import ErgoLibC
+
+/// Box id (32-byte digest)
+class BoxId {
+    internal var pointer: BoxIdPtr
+    
+    /// Parse box id (32 byte digest) from base16-encoded string
+    init(withString str: String) throws {
+        var ptr: BoxIdPtr?
+        let error = str.withCString { cs in
+            ergo_lib_box_id_from_str(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``BoxIdPtr``. Note: we must ensure that no other instance
+    /// of ``BoxId`` can hold this pointer.
+    internal init(withRawPointer ptr: BoxIdPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Returns byte array (32 bytes) representation
+    func toBytes() -> [UInt8] {
+        var bytes = Array.init(repeating: UInt8(0), count: 32)
+        ergo_lib_box_id_to_bytes(self.pointer, &bytes)
+        return bytes
+    }
+     
+    /// Base16 encoded string
+    func toString() -> String {
+        var cStr: UnsafePointer<CChar>?
+        ergo_lib_box_id_to_str(self.pointer, &cStr)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return str
+    }
+    
+    deinit {
+        ergo_lib_box_id_delete(self.pointer)
+    }
+}
+
+extension BoxId: Equatable {
+    static func ==(lhs: BoxId, rhs: BoxId) -> Bool {
+        ergo_lib_box_id_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Box value in nanoERGs with bound checks
+class BoxValue {
+    internal var pointer: BoxValuePtr
+    
+    /// Create from ``Int64`` with bounds check
+    init(fromInt64 : Int64) throws {
+        var ptr: BoxValuePtr?
+        let error = ergo_lib_box_value_from_i64(fromInt64, &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    init(withRawPointer ptr: BoxValuePtr) {
+        self.pointer = ptr
+    }
+    
+    /// Recommended (safe) minimal box value to use in case box size estimation is unavailable.
+    /// Allows box size upto 2777 bytes with current min box value per byte of 360 nanoERGs
+    static func SAFE_USER_MIN() -> BoxValue {
+        var ptr: BoxValuePtr?
+        ergo_lib_box_value_safe_user_min(&ptr)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    /// Number of units inside one ERGO (i.e. one ERG using nano ERG representation)
+    static func UNITS_PER_ERGO() -> Int64 {
+        return ergo_lib_box_value_units_per_ergo()
+    }
+    
+    /// Create a new box value which is the sum of the arguments, throwing error if value is out of
+    /// bounds. Note that swift forbids integer overflow/underflow by default with hard crash, which
+    /// is not desirable here.
+    static func sumOf(boxValue0: BoxValue, boxValue1: BoxValue) throws -> BoxValue {
+        var ptr: BoxValuePtr?
+        let error = ergo_lib_box_value_sum_of(boxValue0.pointer, boxValue1.pointer, &ptr)
+        try checkError(error)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    /// Get value as ``Int64``
+    func toInt64() -> Int64 {
+        return ergo_lib_box_value_as_i64(self.pointer)
+    }
+    
+    deinit {
+        ergo_lib_box_value_delete(self.pointer)
+    }
+}
+
+extension BoxValue: Equatable {
+    static func ==(lhs: BoxValue, rhs: BoxValue) -> Bool {
+        ergo_lib_box_value_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Contains the same fields as ``ErgoBox``, except for transaction id and index, that will be
+/// calculated after full transaction formation.  Use ``ErgoBoxCandidateBuilder`` to create an
+/// instance.
+class ErgoBoxCandidate {
+    internal var pointer: ErgoBoxCandidatePtr
+
+    /// Takes ownership of an existing ``ErgoBoxCandidatePtr``. Note: we must ensure that no other
+    /// instance of ``ErgoBoxCandidate`` can hold this pointer.
+    internal init(withRawPointer pointer: ErgoBoxCandidatePtr) {
+        self.pointer = pointer
+    }
+    
+    /// Returns value (ErgoTree constant) stored in the register or `nil` if the register is empty
+    func getRegisterValue(registerId: NonMandatoryRegisterId) -> Constant? {
+        var constantPtr: ConstantPtr?
+        let res = ergo_lib_ergo_box_candidate_register_value(self.pointer, registerId.rawValue, &constantPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return Constant(withRawPointer: constantPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Get box creation height
+    func getCreationHeight() -> UInt32 {
+        return ergo_lib_ergo_box_candidate_creation_height(self.pointer)
+    }
+
+    /// Get tokens for box
+    func getTokens() -> Tokens {
+        var tokensPtr: TokensPtr?
+        ergo_lib_ergo_box_candidate_tokens(self.pointer, &tokensPtr)
+        return Tokens(withPtr: tokensPtr!)
+    }
+    
+    /// Get ergo tree for box
+    func getErgoTree() -> ErgoTree {
+        var ergoTreePtr: ErgoTreePtr?
+        ergo_lib_ergo_box_candidate_ergo_tree(self.pointer, &ergoTreePtr)
+        return ErgoTree(withRawPointer: ergoTreePtr!)
+    }
+    
+    /// Get box value
+    func getBoxValue() -> BoxValue {
+        var boxValuePtr: BoxValuePtr?
+        ergo_lib_ergo_box_candidate_box_value(self.pointer, &boxValuePtr)
+        return BoxValue(withRawPointer: boxValuePtr!)
+    }
+    
+    deinit {
+        ergo_lib_ergo_box_candidate_delete(self.pointer)
+    }
+}
+
+extension ErgoBoxCandidate: Equatable {
+    static func ==(lhs: ErgoBoxCandidate, rhs: ErgoBoxCandidate) -> Bool {
+        ergo_lib_ergo_box_candidate_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Ergo box, that is taking part in some transaction on the chain Differs with ``ErgoBoxCandidate``
+/// by added transaction id and an index in the input of that transaction
+class ErgoBox{
+    internal var pointer: ErgoBoxPtr
+
+    /// Make a new box.
+    /// - Parameters
+    ///  - `boxValue`: amount of money associated with the box
+    ///  - `creationHeight`: height when a transaction containing the box is created.
+    ///  - `contract`: guarding contract(``Contract``), which should be evaluated to true in order
+    ///    to open(spend) this box
+    ///  - `txId`: transaction id in which this box was "created" (participated in outputs)
+    ///  - `index`: index (in outputs) in the transaction
+    init( boxValue: BoxValue,
+          creationHeight: UInt32,
+          contract: Contract,
+          txId: TxId,
+          index: UInt16,
+          tokens: Tokens
+    ) throws {
+        var ptr: ErgoBoxPtr?
+        let error = ergo_lib_ergo_box_new(
+            boxValue.pointer,
+            creationHeight,
+            contract.pointer,
+            txId.pointer,
+            index,
+            tokens.pointer,
+            &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Parse from JSON.  Supports Ergo Node/Explorer API and box values and token amount encoded as
+    /// strings.
+    init(withJson json: String) throws {
+        var ptr: ErgoBoxPtr?
+        let error = json.withCString { cs in
+            ergo_lib_ergo_box_from_json(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ErgoBoxPtr``. Note: we must ensure that no other instance
+    /// of ``ErgoBox`` can hold this pointer.
+    internal init(withRawPointer pointer: ErgoBoxPtr) {
+        self.pointer = pointer
+    }
+    
+    /// Get box id
+    func getBoxId() -> BoxId {
+        var ptr: BoxIdPtr?
+        ergo_lib_ergo_box_id(self.pointer, &ptr)
+        return BoxId(withRawPointer: ptr!)
+    }
+    
+    /// Get box creation height
+    func getCreationHeight() -> UInt32 {
+        return ergo_lib_ergo_box_creation_height(self.pointer)
+    }
+    
+    /// Get tokens for box
+    func getTokens() -> Tokens {
+        var tokensPtr: TokensPtr?
+        ergo_lib_ergo_box_tokens(self.pointer, &tokensPtr)
+        return Tokens(withPtr: tokensPtr!)
+    }
+    
+    /// Get ergo tree for box
+    func getErgoTree() -> ErgoTree {
+        var ergoTreePtr: ErgoTreePtr?
+        ergo_lib_ergo_box_ergo_tree(self.pointer, &ergoTreePtr)
+        return ErgoTree(withRawPointer: ergoTreePtr!)
+    }
+    
+    /// Get box value
+    func getBoxValue() -> BoxValue {
+        var boxValuePtr: BoxValuePtr?
+        ergo_lib_ergo_box_value(self.pointer, &boxValuePtr)
+        return BoxValue(withRawPointer: boxValuePtr!)
+    }
+    
+    /// Returns value (ErgoTree constant) stored in the register or `nil` if the register is empty
+    func getRegisterValue(registerId: NonMandatoryRegisterId) -> Constant? {
+        var constantPtr: ConstantPtr?
+        let res = ergo_lib_ergo_box_register_value(self.pointer, registerId.rawValue, &constantPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return Constant(withRawPointer: constantPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+    func toJSON() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_ergo_box_to_json(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    /// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+    func toJsonEIP12() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_ergo_box_to_json_eip12(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    deinit {
+        ergo_lib_ergo_box_delete(self.pointer)
+    }
+}
+
+extension ErgoBox: Equatable {
+    static func ==(lhs: ErgoBox, rhs: ErgoBox) -> Bool {
+        ergo_lib_ergo_box_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Pair of (value, tokens) for a box
+class ErgoBoxAssetsData {
+    internal var pointer: ErgoBoxAssetsDataPtr
+
+    /// Create new instance
+    init( boxValue: BoxValue,
+          tokens: Tokens
+    ) {
+        var ptr: ErgoBoxAssetsDataPtr?
+        ergo_lib_ergo_box_assets_data_new(
+            boxValue.pointer,
+            tokens.pointer,
+            &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ErgoBoxAssetsDataPtr``. Note: we must ensure that no other
+    /// instance of ``ErgoBoxAssetsData`` can hold this pointer.
+    internal init(withRawPointer pointer: ErgoBoxAssetsDataPtr) {
+        self.pointer = pointer
+    }
+    
+    /// Value part of the box
+    func getBoxValue() -> BoxValue {
+        var boxValuePtr: BoxValuePtr?
+        ergo_lib_ergo_box_assets_data_value(self.pointer, &boxValuePtr)
+        return BoxValue(withRawPointer: boxValuePtr!)
+    }
+    
+    /// Tokens part of the box
+    func getTokens() -> Tokens {
+        var tokensPtr: TokensPtr?
+        ergo_lib_ergo_box_assets_data_tokens(self.pointer, &tokensPtr)
+        return Tokens(withPtr: tokensPtr!)
+    }
+    
+    deinit {
+        ergo_lib_ergo_box_assets_data_delete(self.pointer)
+    }
+}
+
+extension ErgoBoxAssetsData: Equatable {
+    static func ==(lhs: ErgoBoxAssetsData, rhs: ErgoBoxAssetsData) -> Bool {
+        ergo_lib_ergo_box_assets_data_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// An ordered collection of ``ErgoBoxAssetsData``s
+class ErgoBoxAssetsDataList {
+    internal var pointer: ErgoBoxAssetsDataListPtr
+    
+    /// Create an empty collection
+    init() {
+        var ptr: ErgoBoxAssetsDataListPtr?
+        ergo_lib_ergo_box_assets_data_list_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ErgoBoxAssetsDataListPtr``. Note: we must ensure that no other
+    /// instance of ``ErgoBoxAssetsDataList`` can hold this pointer.
+    init(withRawPointer ptr: ErgoBoxAssetsDataListPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_ergo_box_assets_data_list_len(self.pointer)
+    }
+    
+    /// Returns the ``ErgoBoxAssetsData`` at location `index` if it exists.
+    func get(index: UInt) -> ErgoBoxAssetsData? {
+        var ptr: ErgoBoxAssetsDataPtr?
+        let res = ergo_lib_ergo_box_assets_data_list_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return ErgoBoxAssetsData(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``ErgoBoxAssetsData`` to the end of the collection.
+    func add(ergoBoxAssetData: ErgoBoxAssetsData) {
+        ergo_lib_ergo_box_assets_data_list_add(ergoBoxAssetData.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_ergo_box_assets_data_list_delete(self.pointer)
+    }
+}
+
+/// Type for representing box registers R4 - R9
+enum NonMandatoryRegisterId: UInt8 {
+    /// id for R4 register
+    case R4 = 4
+    /// id for R5 register
+    case R5 = 5
+    /// id for R6 register
+    case R6 = 6
+    /// id for R7 register
+    case R7 = 7
+    /// id for R8 register
+    case R8 = 8
+    /// id for R9 register
+    case R9 = 9
+}
+
+/// An ordered collection of ``ErgoBoxCandidate``s
+class ErgoBoxCandidates {
+    internal var pointer: ErgoBoxCandidatesPtr
+    
+    /// Create an empty collection
+    init() {
+        var ptr: ErgoBoxCandidatesPtr?
+        ergo_lib_ergo_box_candidates_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ErgoBoxCandidatesPtr``. Note: we must ensure that no other
+    /// instance of ``ErgoBoxCandidates`` can hold this pointer.
+    init(withRawPointer ptr: ErgoBoxCandidatesPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_ergo_box_candidates_len(self.pointer)
+    }
+    
+    /// Returns the ``ErgoBoxCandidates`` at location `index` if it exists.
+    func get(index: UInt) -> ErgoBoxCandidate? {
+        var dataInputPtr: DataInputPtr?
+        let res = ergo_lib_ergo_box_candidates_get(self.pointer, index, &dataInputPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return ErgoBoxCandidate(withRawPointer: dataInputPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``ErgoBoxCandidates`` to the end of the collection.
+    func add(ergoBoxCandidate: ErgoBoxCandidate) {
+        ergo_lib_ergo_box_candidates_add(ergoBoxCandidate.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_ergo_box_candidates_delete(self.pointer)
+    }
+}
+
+/// An ordered collection of ``ErgoBox``s
+class ErgoBoxes {
+    internal var pointer: ErgoBoxesPtr
+    
+    init() {
+        self.pointer = ErgoBoxes.initRawPtrEmpty()
+    }
+    
+    /// Parse ``ErgoBox`` array from JSON (Node API)
+    init(fromJSON: Any) throws {
+        let json = JSON(fromJSON)
+        if let arr = json.array {
+            let boxes = try arr.map{ elem -> ErgoBox in
+                if let jsonStr = elem.rawString() {
+                    return try ErgoBox(withJson: jsonStr);
+                } else {
+                    throw WalletError.walletCError(reason: "Ergoboxes.init(fromJSON): cannot cast array element to raw JSON string")
+                }
+            }
+            self.pointer = ErgoBoxes.initRawPtrEmpty()
+            for ergoBox in boxes {
+                self.add(ergoBox: ergoBox)
+            }
+        } else {
+            throw WalletError.walletCError(reason: "Ergoboxes.init(fromJSON): expected [JSON]")
+        }
+    }
+    
+    /// Takes ownership of an existing ``ErgoBoxesPtr``. Note: we must ensure that no other
+    /// instance of ``ErgoBoxes`` can hold this pointer.
+    init(withRawPointer ptr: ErgoBoxesPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Use the C-API to create an empty collection and return the raw pointer that points to this
+    /// collection.
+    private static func initRawPtrEmpty() -> ErgoBoxesPtr {
+        var ptr: ErgoBoxesPtr?
+        ergo_lib_ergo_boxes_new(&ptr)
+        return ptr!
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_ergo_boxes_len(self.pointer)
+    }
+    
+    /// Returns the ``BlockHeader`` at location `index` if it exists.
+    func get(index: UInt) -> ErgoBox? {
+        var dataInputPtr: DataInputPtr?
+        let res = ergo_lib_ergo_boxes_get(self.pointer, index, &dataInputPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return ErgoBox(withRawPointer: dataInputPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``ErgoBox`` to the end of the collection.
+    func add(ergoBox: ErgoBox) {
+        ergo_lib_ergo_boxes_add(ergoBox.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_ergo_boxes_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoBoxCandidateBuilder.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoBoxCandidateBuilder.swift
new file mode 100644
index 000000000..09f304b10
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoBoxCandidateBuilder.swift
@@ -0,0 +1,161 @@
+import Foundation
+import ErgoLibC
+
+/// ``ErgoBoxCandidate`` builder
+class ErgoBoxCandidateBuilder {
+    internal var pointer: ErgoBoxCandidateBuilderPtr
+    /// Create builder with required box parameters.
+    /// - Parameters
+    ///  - `boxValue`:  amount of money associated with the box
+    ///  - `contract`: guarding contract([`Contract`]), which should be evaluated to true in order
+    ///    to open(spend) this box
+    ///  - `creationHeight`: height when a transaction containing the box is created.
+    /// It should not exceed height of the block, containing the transaction with this box.
+    init(boxValue: BoxValue, contract: Contract, creationHeight: UInt32) {
+        var ptr: ErgoBoxCandidateBuilderPtr?
+        ergo_lib_ergo_box_candidate_builder_new(
+            boxValue.pointer,
+            contract.pointer,
+            creationHeight,
+            &ptr
+        )
+        self.pointer = ptr!
+    }
+    
+    /// Set minimal value (per byte of the serialized box size)
+    func setMinBoxValuePerByte(minBoxValuePerByte: UInt32) -> ErgoBoxCandidateBuilder {
+        ergo_lib_ergo_box_candidate_builder_set_min_box_value_per_byte(
+            self.pointer,
+            minBoxValuePerByte
+        )
+        return self
+    }
+    
+    /// Get minimal value (per byte of the serialized box size)
+    func getMinBoxValuePerByte() -> UInt32 {
+        ergo_lib_ergo_box_candidate_builder_min_box_value_per_byte(self.pointer)
+    }
+    
+    /// Set new box value
+    func setValue(boxValue: BoxValue) -> ErgoBoxCandidateBuilder {
+        ergo_lib_ergo_box_candidate_builder_set_value(
+            self.pointer,
+            boxValue.pointer
+        )
+        return self
+    }
+    
+    /// Get box value
+    func getValue() -> BoxValue {
+        var ptr: BoxValuePtr?
+        ergo_lib_ergo_box_candidate_builder_value(self.pointer, &ptr)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    /// Calculate serialized box size(in bytes)
+    func calcBoxSizeBytes() throws -> UInt {
+        let res = ergo_lib_ergo_box_candidate_builder_calc_box_size_bytes(self.pointer)
+        try checkError(res.error)
+        return res.value
+    }
+    
+    /// Calculate minimal box value for the current box serialized size(in bytes)
+    func calcMinBoxValue() throws -> BoxValue {
+        var ptr: BoxValuePtr?
+        let error = ergo_lib_ergo_box_candidate_calc_min_box_value(self.pointer, &ptr)
+        try checkError(error)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    /// Set register with a given id (R4-R9) to the given value
+    func setRegisterValue(
+        registerId: NonMandatoryRegisterId,
+        constant: Constant
+    ) -> ErgoBoxCandidateBuilder {
+        ergo_lib_ergo_box_candidate_builder_set_register_value(
+            self.pointer,
+            registerId.rawValue,
+            constant.pointer
+        )
+        return self
+    }
+    
+    /// Returns register value for the given register id (R4-R9), or None if the register is empty
+    func getRegisterValue(registerId: NonMandatoryRegisterId) -> Constant? {
+        var ptr: ConstantPtr?
+        let res = ergo_lib_ergo_box_candidate_builder_register_value(
+            self.pointer,
+            registerId.rawValue,
+            &ptr
+        )
+        assert(res.error == nil)
+        if res.is_some {
+            return Constant(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Delete register value(make register empty) for the given register id (R4-R9)
+    func deleteRegisterValue(
+        registerId: NonMandatoryRegisterId
+    ) -> ErgoBoxCandidateBuilder {
+        ergo_lib_ergo_box_candidate_builder_delete_register_value(
+            self.pointer,
+            registerId.rawValue
+        )
+        return self
+    }
+    
+    /// Mint token, as defined in https://github.com/ergoplatform/eips/blob/master/eip-0004.md
+    /// - Parameters
+    ///  - `token`: token id(box id of the first input box in transaction) and token amount,
+    ///  - `tokenName`: token name (will be encoded in R4),
+    ///  - `tokenDesc`: token description (will be encoded in R5),
+    ///  - `numDecimals`: number of decimals (will be encoded in R6)
+    func mintToken(
+        token: Token,
+        tokenName: String,
+        tokenDesc: String,
+        numDecimals: UInt
+    ) -> ErgoBoxCandidateBuilder {
+        let _ =
+            tokenName.withCString{tokenNameCStr in
+                tokenDesc.withCString{tokenDescCStr in
+                    ergo_lib_ergo_box_candidate_builder_mint_token(
+                        self.pointer,
+                        token.pointer,
+                        tokenNameCStr,
+                        tokenDescCStr,
+                        numDecimals
+                    )
+                }
+            }
+        return self
+    }
+    
+    /// Add given token id and token amount
+    func addToken(
+        tokenId: TokenId,
+        tokenAmount: TokenAmount
+    ) -> ErgoBoxCandidateBuilder {
+        ergo_lib_ergo_box_candidate_builder_add_token(
+            self.pointer,
+            tokenId.pointer,
+            tokenAmount.pointer
+        )
+        return self
+    }
+    
+    /// Build the box candidate
+    func build() throws -> ErgoBoxCandidate {
+        var ptr: ErgoBoxCandidatePtr?
+        let error = ergo_lib_ergo_box_candidate_builder_build(self.pointer, &ptr)
+        try checkError(error)
+        return ErgoBoxCandidate(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_ergo_box_candidate_builder_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoLib.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoLib.swift
index ec303a37a..a7ddcf71c 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoLib.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoLib.swift
@@ -1,173 +1,20 @@
 import Foundation
 import ErgoLibC
 
+/// Simple wrapper for errors emitted from `ergo-lib`
 enum WalletError: Error {
     case walletCError(reason: String)
 }
 
-class UnspentBoxes {
-    internal var pointer: UnspentBoxesPtr
-
-    init(withJson json: String) throws {
-        self.pointer = try UnspentBoxes.from_json(json: json)
-    }
-
-    private static func from_json(json: String) throws -> UnspentBoxesPtr {
-        var unspentBoxesPtr: UnspentBoxesPtr?
-        let error = json.withCString { cs in
-            ergo_wallet_unspent_boxes_from_json(cs, &unspentBoxesPtr)
-        }
-        try checkError(error)
-        return unspentBoxesPtr!
-    }
-
-    deinit {
-        ergo_wallet_unspent_boxes_delete(self.pointer)
-    }
-}
-
-private func checkError(_ error: ErrorPtr?) throws {
+internal func checkError(_ error: ErrorPtr?) throws {
     if error == nil {
         return
     }
 
-    let cStringReason = ergo_wallet_error_to_string(error)
+    let cStringReason = ergo_lib_error_to_string(error)
     let reason = String(cString: cStringReason!)
-    ergo_wallet_delete_string(cStringReason)
-    ergo_wallet_delete_error(error)
+    ergo_lib_delete_string(cStringReason)
+    ergo_lib_delete_error(error)
     throw WalletError.walletCError(reason: reason)
 }
 
-class Transaction {
-    private var pointer: TransactionPtr
-
-    internal init(withRawPointer pointer: TransactionPtr) {
-        self.pointer = pointer
-    }
-
-    func toJson() throws -> String {
-        var cStr: UnsafePointer<CChar>?
-        let error = ergo_wallet_signed_tx_to_json(self.pointer, &cStr)
-        try checkError(error)
-        let str = String(cString: cStr!)
-        ergo_wallet_delete_string(UnsafeMutablePointer(mutating: cStr))
-        return str
-    }
-
-    deinit {
-        ergo_wallet_delete_signed_tx(self.pointer)
-    }
-}
-
-class ErgoStateContext {
-    internal var pointer: ErgoStateContextPtr
-
-    init(withJson json: String) throws {
-        self.pointer = try ErgoStateContext.fromJson(json: json)
-    }
-
-    private static func fromJson(json: String) throws -> ErgoStateContextPtr {
-        var ergoStateContextPtr: ErgoStateContextPtr?
-        let error = json.withCString { cs in
-            ergo_wallet_ergo_state_context_from_json(cs, &ergoStateContextPtr)
-        }
-        try checkError(error)
-        return ergoStateContextPtr!
-    }
-
-    deinit {
-        ergo_wallet_ergo_state_context_delete(self.pointer)
-    }
-}
-
-class Address {
-    internal var pointer: AddressPtr
-
-    init(withTestnetAddress addressStr: String) throws {
-        self.pointer = try Address.fromTestnetAddress(addressStr: addressStr)
-    }
-
-    private static func fromTestnetAddress(addressStr: String) throws -> AddressPtr {
-        var ptr: AddressPtr?
-        let error = addressStr.withCString { cs in
-            ergo_wallet_address_from_testnet(cs, &ptr)
-        }
-        try checkError(error)
-        return ptr!
-    }
-    
-    deinit {
-        ergo_wallet_address_delete(self.pointer)
-    }
-}
-
-class ErgoBoxCandidate {
-    internal var pointer: ErgoBoxCandidatePtr
-
-    internal init(withRawPointer pointer: ErgoBoxCandidatePtr) {
-        self.pointer = pointer
-    }
-
-    static func payToAddress(recipient: Address,
-                             value: UInt64, creationHeight: UInt32) throws -> ErgoBoxCandidate {
-        var ergoBoxCandidatePtr: ErgoBoxCandidatePtr?
-        let error = ergo_wallet_ergo_box_candidate_new_pay_to_address(recipient.pointer, 
-                                                                     value,
-                                                                     creationHeight,
-                                                                     &ergoBoxCandidatePtr)
-        try checkError(error)
-        return ErgoBoxCandidate(withRawPointer: ergoBoxCandidatePtr!)
-    }
-
-    deinit {
-        ergo_wallet_ergo_box_candidate_delete(self.pointer)
-    }
-}
-
-class OutputBoxes {
-    internal var pointer: OutputBoxesPtr
-
-    init(box: ErgoBoxCandidate) throws {
-        var ptr: OutputBoxesPtr?
-        let error = ergo_wallet_output_boxes_new(box.pointer, &ptr)
-        try checkError(error)
-        self.pointer = ptr!
-    }
-
-    deinit {
-        ergo_wallet_output_boxes_delete(self.pointer)
-    }
-}
-
-class Wallet {
-    internal var pointer: WalletPtr
-
-    init(withMnemonic mnemonicPhrase: String) throws {
-        var ptr: WalletPtr?
-        let error = mnemonicPhrase.withCString { mnemonicPhraseC in
-            ergo_wallet_wallet_from_mnemonic(mnemonicPhraseC, nil, 0, &ptr)
-        }
-        try checkError(error)
-        self.pointer = ptr!
-    }
-
-    func new_signed_tx(ergoStateContext: ErgoStateContext, 
-                              unspentBoxes: UnspentBoxes, 
-                              outputBoxes: OutputBoxes,
-                              sendChangeTo: Address, 
-                              minChangeValue: UInt64,
-                              txFeeAmount: UInt64) throws -> Transaction {
-        var transactionPtr: TransactionPtr?
-        let error = ergo_wallet_wallet_new_signed_tx(self.pointer, 
-                                              ergoStateContext.pointer,
-                                              unspentBoxes.pointer, 
-                                              nil, // data input boxes
-                                              outputBoxes.pointer,
-                                              sendChangeTo.pointer,
-                                              minChangeValue, 
-                                              txFeeAmount, 
-                                              &transactionPtr)
-        try checkError(error)
-        return Transaction(withRawPointer: transactionPtr!)
-    }
-}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoStateContext.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoStateContext.swift
new file mode 100644
index 000000000..7c2741bb5
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoStateContext.swift
@@ -0,0 +1,25 @@
+
+import Foundation
+import ErgoLibC
+
+/// Blockchain state (last headers, etc.)
+class ErgoStateContext {
+    internal var pointer: ErgoStateContextPtr
+    
+    /// Create new context
+    init(preHeader : PreHeader, headers: BlockHeaders) throws {
+        var ptr: ErgoStateContextPtr?
+        let error = ergo_lib_ergo_state_context_new(preHeader.pointer, headers.pointer, &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    deinit {
+        ergo_lib_ergo_state_context_delete(self.pointer)
+    }
+}
+extension ErgoStateContext: Equatable {
+    static func ==(lhs: ErgoStateContext, rhs: ErgoStateContext) -> Bool {
+        ergo_lib_ergo_state_context_eq(lhs.pointer, rhs.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoTree.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoTree.swift
new file mode 100644
index 000000000..3761a1a83
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ErgoTree.swift
@@ -0,0 +1,109 @@
+import Foundation
+import ErgoLibC
+
+/// The root of ErgoScript IR. Serialized instances of this class are self sufficient and can be passed around.
+class ErgoTree {
+    internal var pointer: ErgoTreePtr
+    
+    /// Decode from encoded serialized ``ErgoTree``
+    init(fromBytes : [UInt8]) throws {
+        var ptr: ErgoTreePtr?
+        let error = ergo_lib_ergo_tree_from_bytes(
+            fromBytes,
+            UInt(fromBytes.count),
+            &ptr
+        )
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Decode from base16 encoded serialized ``ErgoTree``
+    init(fromBase16EncodedString : String) throws {
+        var ptr: ErgoTreePtr?
+        let error = fromBase16EncodedString.withCString { cs in
+            ergo_lib_ergo_tree_from_base16_bytes(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``ErgoTreePtr``. Note: we must ensure that no other instance
+    /// of ``ErgoTree`` can hold this pointer.
+    init(withRawPointer ptr: ErgoTreePtr) {
+        self.pointer = ptr
+    }
+    
+    /// Convert to serialized bytes.
+    func toBytes() throws -> [UInt8] {
+        let res = ergo_lib_ergo_tree_bytes_len(self.pointer)
+        try checkError(res.error)
+        var bytes = Array.init(repeating: UInt8(0), count: Int(res.value))
+        let error = ergo_lib_ergo_tree_to_bytes(self.pointer, &bytes)
+        try checkError(error)
+        return bytes
+    }
+    
+    /// Convert to base16-encoded serialized bytes
+    func toBase16EncodedString() throws -> String {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_ergo_tree_to_base16_bytes(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return str
+    }
+    
+    /// Returns the number of constants stored in the serialized ``ErgoTree`` or throws error if the
+    /// parsing of constants failed
+    func constantsLength() throws -> UInt {
+        let res = ergo_lib_ergo_tree_constants_len(self.pointer)
+        try checkError(res.error)
+        return res.value
+    }
+    
+    /// Return constant with given index (as stored in serialized ErgoTree) if it exists. Throws if
+    /// constant parsing failed.
+    func getConstant(index: UInt) throws -> Constant? {
+        var constantPtr: ConstantPtr?
+        let res = ergo_lib_ergo_tree_get_constant(self.pointer, index, &constantPtr)
+        try checkError(res.error)
+        if res.is_some {
+            return Constant(withRawPointer: constantPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Replace the constant of the ``ErgoTree`` with the given `constant` at position `index`.
+    /// Throws if no constant exists at `index`.
+    func withConstant(index: UInt, constant: Constant) throws {
+        var newErgoTreePtr: ErgoTreePtr?
+        let error = ergo_lib_ergo_tree_with_constant(self.pointer, index, constant.pointer, &newErgoTreePtr)
+        try checkError(error)
+        
+        // Delete the old ErgoTree and point to new instance
+        ergo_lib_ergo_tree_delete(self.pointer)
+        self.pointer = newErgoTreePtr!
+    }
+    
+    /// Serialized proposition expression of SigmaProp type with ConstantPlaceholder nodes instead of
+    /// Constant nodes.
+    func toTemplateBytes() throws -> [UInt8] {
+        let res = ergo_lib_ergo_tree_template_bytes_len(self.pointer)
+        try checkError(res.error)
+        var bytes = Array.init(repeating: UInt8(0), count: Int(res.value))
+        let error = ergo_lib_ergo_tree_template_bytes(self.pointer, &bytes)
+        try checkError(error)
+        return bytes
+    }
+    
+    deinit {
+        ergo_lib_ergo_tree_delete(self.pointer)
+    }
+}
+
+extension ErgoTree: Equatable {
+    static func ==(lhs: ErgoTree, rhs: ErgoTree) -> Bool {
+        ergo_lib_ergo_tree_eq(lhs.pointer, rhs.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Input.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Input.swift
new file mode 100644
index 000000000..fc8578223
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Input.swift
@@ -0,0 +1,186 @@
+import Foundation
+import ErgoLibC
+
+/// Unsigned inputs used in constructing unsigned transactions
+class UnsignedInput {
+    internal var pointer: UnsignedInputPtr
+    
+    /// Takes ownership of an existing ``UnsignedInputPtr``. Note: we must ensure that no other instance
+    /// of ``UnsignedInput`` can hold this pointer.
+    internal init(withRawPointer ptr: UnsignedInputPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get box id
+    func getBoxId() -> BoxId {
+        var boxIdPtr: BoxIdPtr?
+        ergo_lib_unsigned_input_box_id(self.pointer, &boxIdPtr)
+        return BoxId(withRawPointer: boxIdPtr!)
+    }
+        
+    /// Get context extension
+    func getContextExtension() -> ContextExtension {
+        var contextExtensionPtr: ContextExtensionPtr?
+        ergo_lib_unsigned_input_context_extension(self.pointer, &contextExtensionPtr)
+        return ContextExtension(withPtr: contextExtensionPtr!)
+    }
+    
+    deinit {
+        ergo_lib_unsigned_input_delete(self.pointer)
+    }
+}
+
+/// Signed inputs used in signed transactions
+class Input {
+    internal var pointer: InputPtr
+    
+    internal init(withRawPointer ptr: InputPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get box id
+    func getBoxId() -> BoxId {
+        var boxIdPtr: BoxIdPtr?
+        ergo_lib_input_box_id(self.pointer, &boxIdPtr)
+        return BoxId(withRawPointer: boxIdPtr!)
+    }
+        
+    /// Get spending proof
+    func getSpendingProof() -> ProverResult {
+        var ptr: ProverResultPtr?
+        ergo_lib_input_spending_proof(self.pointer, &ptr)
+        return ProverResult(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_input_delete(self.pointer)
+    }
+}
+
+/// Proof of correctness of tx spending
+class ProverResult {
+    internal var pointer: ProverResultPtr
+    
+    internal init(withRawPointer ptr: ProverResultPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get proof bytes
+    func toBytes() -> [UInt8] {
+        let proofLength = ergo_lib_prover_result_proof_len(self.pointer)
+        var bytes = Array.init(repeating: UInt8(0), count: Int(proofLength))
+        ergo_lib_prover_result_proof(self.pointer, &bytes)
+        return bytes
+    }
+    
+    /// Get context extension
+    func getContextExtension() -> ContextExtension {
+        var ptr: ContextExtensionPtr?
+        ergo_lib_prover_result_context_extension(self.pointer, &ptr)
+        return ContextExtension(withPtr: ptr!)
+    }
+    
+    /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+    func toJSON() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_prover_result_to_json(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    deinit {
+        ergo_lib_prover_result_delete(self.pointer)
+    }
+}
+
+/// An ordered collection of ``UnsignedInput``s
+class UnsignedInputs {
+    internal var pointer: UnsignedInputsPtr
+    
+    /// Create an empty collection
+    init() {
+        var ptr: UnsignedInputsPtr?
+        ergo_lib_unsigned_inputs_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``UnsignedInputsPtr``. Note: we must ensure that no other instance
+    /// of ``UnsignedInputs`` can hold this pointer.
+    internal init(withRawPointer ptr: UnsignedInputsPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_unsigned_inputs_len(self.pointer)
+    }
+    
+    /// Returns the ``UnsignedInput`` at location `index` if it exists.
+    func get(index: UInt) -> UnsignedInput? {
+        var ptr: UnsignedInputPtr?
+        let res = ergo_lib_unsigned_inputs_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return UnsignedInput(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add an ``UnsignedInput`` to the end of the collection.
+    func add(unsignedInput: UnsignedInput) {
+        ergo_lib_unsigned_inputs_add(unsignedInput.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_unsigned_inputs_delete(self.pointer)
+    }
+}
+
+/// An ordered collection of ``Input``s
+class Inputs {
+    internal var pointer: InputsPtr
+    
+    /// Create an empty collection
+    init() {
+        var ptr: InputsPtr?
+        ergo_lib_inputs_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``InputsPtr``. Note: we must ensure that no other instance
+    /// of ``Inputs`` can hold this pointer.
+    init(withRawPointer ptr: InputsPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_inputs_len(self.pointer)
+    }
+    
+    /// Returns the ``Input`` at location `index` if it exists.
+    func get(index: UInt) -> Input? {
+        var ptr: InputPtr?
+        let res = ergo_lib_inputs_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return Input(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add an ``Input`` to the end of the collection.
+    func add(input: Input) {
+        ergo_lib_inputs_add(input.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_inputs_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/PreHeader.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/PreHeader.swift
new file mode 100644
index 000000000..6c0f6ac2e
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/PreHeader.swift
@@ -0,0 +1,25 @@
+import Foundation
+import ErgoLibC
+
+/// Block header with the current `spendingTransaction`, that can be predicted by a miner before its
+/// formation
+class PreHeader {
+    internal var pointer: PreHeaderPtr
+    
+    /// Create instance using data from block header
+    init(withBlockHeader blockHeader: BlockHeader) {
+        var preHeaderPtr: PreHeaderPtr?
+        ergo_lib_preheader_from_block_header(blockHeader.pointer, &preHeaderPtr)
+        self.pointer = preHeaderPtr!
+    }
+    
+    deinit {
+        ergo_lib_preheader_delete(self.pointer)
+    }
+}
+
+extension PreHeader: Equatable {
+    static func ==(lhs: PreHeader, rhs: PreHeader) -> Bool {
+        ergo_lib_pre_header_eq(lhs.pointer, rhs.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift
new file mode 100644
index 000000000..6dbca514a
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/ReducedTransaction.swift
@@ -0,0 +1,44 @@
+import Foundation
+import ErgoLibC
+
+/// Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input
+/// is augmented with ReducedInput which contains a script reduction result.
+/// After an unsigned transaction is reduced it can be signed without context.
+/// Thus, it can be serialized and transferred for example to Cold Wallet and signed
+/// in an environment where secrets are known.
+/// see EIP-19 for more details -
+/// https://github.com/ergoplatform/eips/blob/f280890a4163f2f2e988a0091c078e36912fc531/eip-0019.md
+class ReducedTransaction {
+    internal var pointer: ReducedTransactionPtr
+    
+    /// Create `reduced` transaction, i.e. unsigned transaction where each unsigned input
+    /// is augmented with ReducedInput which contains a script reduction result.
+    init(
+        unsignedTx: UnsignedTransaction,
+        boxesToSpend: ErgoBoxes,
+        dataBoxes: ErgoBoxes,
+        stateContext: ErgoStateContext
+    ) throws {
+        var ptr: ReducedTransactionPtr?
+        let error = ergo_lib_reduced_tx_from_unsigned_tx(
+            unsignedTx.pointer,
+            boxesToSpend.pointer,
+            dataBoxes.pointer,
+            stateContext.pointer,
+            &ptr
+        )
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Returns the unsigned transation
+    func getUnsignedTransaction() -> UnsignedTransaction {
+        var ptr: UnsignedTransactionPtr?
+        ergo_lib_reduced_tx_unsigned_tx(self.pointer, &ptr)
+        return UnsignedTransaction(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_reduced_tx_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/SecretKey.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/SecretKey.swift
new file mode 100644
index 000000000..b209a462e
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/SecretKey.swift
@@ -0,0 +1,84 @@
+import Foundation
+import ErgoLibC
+
+/// Secret key for the prover
+class SecretKey {
+    internal var pointer: SecretKeyPtr
+    
+    /// Generate random key
+    init() {
+        var ptr: SecretKeyPtr?
+        ergo_lib_secret_key_generate_random(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Parse dlog secret key from bytes (SEC-1-encoded scalar)
+    init(fromBytes : [UInt8]) throws {
+        var ptr: SecretKeyPtr?
+        let error = ergo_lib_secret_key_from_bytes(fromBytes, &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``SecretKeyPtr``. Note: we must ensure that no other instance
+    /// of ``SecretKey`` can hold this pointer.
+    internal init(withRawPointer ptr: BlockHeaderPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get address (encoded public image)
+    func getAddress() -> Address {
+        var ptr: AddressPtr?
+        ergo_lib_secret_key_get_address(self.pointer, &ptr)
+        return Address(withRawPointer: ptr!)
+    }
+    
+    /// Encode to bytes.
+    func toBytes() -> [UInt8] {
+        var bytes = Array.init(repeating: UInt8(0), count: 32)
+        ergo_lib_secret_key_to_bytes(self.pointer, &bytes)
+        return bytes
+    }
+    
+    deinit {
+        ergo_lib_secret_key_delete(self.pointer)
+    }
+}
+
+/// An ordered collection of ``SecretKey``s
+class SecretKeys {
+    internal var pointer: SecretKeysPtr
+    
+    /// Create an empty collection
+    init() {
+        var ptr: SecretKeysPtr?
+        ergo_lib_secret_keys_new(&ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_secret_keys_len(self.pointer)
+    }
+    
+    /// Returns the ``SecretKey`` at location `index` if it exists.
+    func get(index: UInt) -> SecretKey? {
+        var ptr: SecretKeyPtr?
+        let res = ergo_lib_secret_keys_get(self.pointer, index, &ptr)
+        assert(res.error == nil)
+        if res.is_some {
+            return SecretKey(withRawPointer: ptr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``SecretKey`` to the end of the collection.
+    func add(secretKey: SecretKey) {
+        ergo_lib_secret_keys_add(secretKey.pointer, self.pointer)
+    }
+        
+    deinit {
+        ergo_lib_secret_keys_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/SwiftyJSON.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/SwiftyJSON.swift
new file mode 100644
index 000000000..c08a26c25
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/SwiftyJSON.swift
@@ -0,0 +1,1401 @@
+//  SwiftyJSON.swift
+//
+//  Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+
+// MARK: - Error
+// swiftlint:disable line_length
+public enum SwiftyJSONError: Int, Swift.Error {
+    case unsupportedType = 999
+    case indexOutOfBounds = 900
+    case elementTooDeep = 902
+    case wrongType = 901
+    case notExist = 500
+    case invalidJSON = 490
+}
+
+extension SwiftyJSONError: CustomNSError {
+
+    /// return the error domain of SwiftyJSONError
+    public static var errorDomain: String { return "com.swiftyjson.SwiftyJSON" }
+
+    /// return the error code of SwiftyJSONError
+    public var errorCode: Int { return self.rawValue }
+
+    /// return the userInfo of SwiftyJSONError
+    public var errorUserInfo: [String: Any] {
+        switch self {
+        case .unsupportedType:
+            return [NSLocalizedDescriptionKey: "It is an unsupported type."]
+        case .indexOutOfBounds:
+            return [NSLocalizedDescriptionKey: "Array Index is out of bounds."]
+        case .wrongType:
+            return [NSLocalizedDescriptionKey: "Couldn't merge, because the JSONs differ in type on top level."]
+        case .notExist:
+            return [NSLocalizedDescriptionKey: "Dictionary key does not exist."]
+        case .invalidJSON:
+            return [NSLocalizedDescriptionKey: "JSON is invalid."]
+        case .elementTooDeep:
+            return [NSLocalizedDescriptionKey: "Element too deep. Increase maxObjectDepth and make sure there is no reference loop."]
+        }
+    }
+}
+
+// MARK: - JSON Type
+
+/**
+JSON's type definitions.
+
+See http://www.json.org
+*/
+public enum Type: Int {
+    case number
+    case string
+    case bool
+    case array
+    case dictionary
+    case null
+    case unknown
+}
+
+// MARK: - JSON Base
+
+public struct JSON {
+
+    /**
+     Creates a JSON using the data.
+    
+     - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary
+     - parameter opt: The JSON serialization reading options. `[]` by default.
+    
+     - returns: The created JSON
+     */
+    public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws {
+        let object: Any = try JSONSerialization.jsonObject(with: data, options: opt)
+        self.init(jsonObject: object)
+    }
+
+    /**
+     Creates a JSON object
+     - note: this does not parse a `String` into JSON, instead use `init(parseJSON: String)`
+    
+     - parameter object: the object
+
+     - returns: the created JSON object
+     */
+    public init(_ object: Any) {
+        switch object {
+        case let object as Data:
+            do {
+                try self.init(data: object)
+            } catch {
+                self.init(jsonObject: NSNull())
+            }
+        default:
+            self.init(jsonObject: object)
+        }
+    }
+
+    /**
+     Parses the JSON string into a JSON object
+    
+     - parameter json: the JSON string
+    
+     - returns: the created JSON object
+    */
+    public init(parseJSON jsonString: String) {
+        if let data = jsonString.data(using: .utf8) {
+            self.init(data)
+        } else {
+            self.init(NSNull())
+        }
+    }
+
+    /**
+     Creates a JSON using the object.
+    
+     - parameter jsonObject:  The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity.
+    
+     - returns: The created JSON
+     */
+    fileprivate init(jsonObject: Any) {
+        object = jsonObject
+    }
+
+    /**
+     Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added,
+     present values getting overwritten, array values getting appended and nested JSONs getting merged the same way.
+ 
+     - parameter other: The JSON which gets merged into this JSON
+    
+     - throws `ErrorWrongType` if the other JSONs differs in type on the top level.
+     */
+    public mutating func merge(with other: JSON) throws {
+        try self.merge(with: other, typecheck: true)
+    }
+
+    /**
+     Merges another JSON into this JSON and returns a new JSON, whereas primitive values which are not present in this JSON are getting added,
+     present values getting overwritten, array values getting appended and nested JSONS getting merged the same way.
+    
+     - parameter other: The JSON which gets merged into this JSON
+    
+     - throws `ErrorWrongType` if the other JSONs differs in type on the top level.
+    
+     - returns: New merged JSON
+     */
+    public func merged(with other: JSON) throws -> JSON {
+        var merged = self
+        try merged.merge(with: other, typecheck: true)
+        return merged
+    }
+
+    /**
+     Private woker function which does the actual merging
+     Typecheck is set to true for the first recursion level to prevent total override of the source JSON
+     */
+     fileprivate mutating func merge(with other: JSON, typecheck: Bool) throws {
+        if type == other.type {
+            switch type {
+            case .dictionary:
+                for (key, _) in other {
+                    try self[key].merge(with: other[key], typecheck: false)
+                }
+            case .array:
+                self = JSON(arrayValue + other.arrayValue)
+            default:
+                self = other
+            }
+        } else {
+            if typecheck {
+                throw SwiftyJSONError.wrongType
+            } else {
+                self = other
+            }
+        }
+    }
+
+    /// Private object
+    fileprivate var rawArray: [Any] = []
+    fileprivate var rawDictionary: [String: Any] = [:]
+    fileprivate var rawString: String = ""
+    fileprivate var rawNumber: NSNumber = 0
+    fileprivate var rawNull: NSNull = NSNull()
+    fileprivate var rawBool: Bool = false
+
+    /// JSON type, fileprivate setter
+    public fileprivate(set) var type: Type = .null
+
+    /// Error in JSON, fileprivate setter
+    public fileprivate(set) var error: SwiftyJSONError?
+
+    /// Object in JSON
+    public var object: Any {
+        get {
+            switch type {
+            case .array:      return rawArray
+            case .dictionary: return rawDictionary
+            case .string:     return rawString
+            case .number:     return rawNumber
+            case .bool:       return rawBool
+            default:          return rawNull
+            }
+        }
+        set {
+            error = nil
+            switch unwrap(newValue) {
+            case let number as NSNumber:
+                if number.isBool {
+                    type = .bool
+                    rawBool = number.boolValue
+                } else {
+                    type = .number
+                    rawNumber = number
+                }
+            case let string as String:
+                type = .string
+                rawString = string
+            case _ as NSNull:
+                type = .null
+            case nil:
+                type = .null
+            case let array as [Any]:
+                type = .array
+                rawArray = array
+            case let dictionary as [String: Any]:
+                type = .dictionary
+                rawDictionary = dictionary
+            default:
+                type = .unknown
+                error = SwiftyJSONError.unsupportedType
+            }
+        }
+    }
+
+    /// The static null JSON
+    @available(*, unavailable, renamed:"null")
+    public static var nullJSON: JSON { return null }
+    public static var null: JSON { return JSON(NSNull()) }
+}
+
+/// Private method to unwarp an object recursively
+private func unwrap(_ object: Any) -> Any {
+    switch object {
+    case let json as JSON:
+        return unwrap(json.object)
+    case let array as [Any]:
+        return array.map(unwrap)
+    case let dictionary as [String: Any]:
+        var d = dictionary
+        dictionary.forEach { pair in
+            d[pair.key] = unwrap(pair.value)
+        }
+        return d
+    default:
+        return object
+    }
+}
+
+public enum Index<T: Any>: Comparable {
+    case array(Int)
+    case dictionary(DictionaryIndex<String, T>)
+    case null
+
+    static public func == (lhs: Index, rhs: Index) -> Bool {
+        switch (lhs, rhs) {
+        case (.array(let left), .array(let right)):           return left == right
+        case (.dictionary(let left), .dictionary(let right)): return left == right
+        case (.null, .null):                                  return true
+        default:                                              return false
+        }
+    }
+
+    static public func < (lhs: Index, rhs: Index) -> Bool {
+        switch (lhs, rhs) {
+        case (.array(let left), .array(let right)):           return left < right
+        case (.dictionary(let left), .dictionary(let right)): return left < right
+        default:                                              return false
+        }
+    }
+}
+
+public typealias JSONIndex = Index<JSON>
+public typealias JSONRawIndex = Index<Any>
+
+extension JSON: Swift.Collection {
+
+    public typealias Index = JSONRawIndex
+
+    public var startIndex: Index {
+        switch type {
+        case .array:      return .array(rawArray.startIndex)
+        case .dictionary: return .dictionary(rawDictionary.startIndex)
+        default:          return .null
+        }
+    }
+
+    public var endIndex: Index {
+        switch type {
+        case .array:      return .array(rawArray.endIndex)
+        case .dictionary: return .dictionary(rawDictionary.endIndex)
+        default:          return .null
+        }
+    }
+
+    public func index(after i: Index) -> Index {
+        switch i {
+        case .array(let idx):      return .array(rawArray.index(after: idx))
+        case .dictionary(let idx): return .dictionary(rawDictionary.index(after: idx))
+        default:                   return .null
+        }
+    }
+
+    public subscript (position: Index) -> (String, JSON) {
+        switch position {
+        case .array(let idx):      return (String(idx), JSON(rawArray[idx]))
+        case .dictionary(let idx): return (rawDictionary[idx].key, JSON(rawDictionary[idx].value))
+        default:                   return ("", JSON.null)
+        }
+    }
+}
+
+// MARK: - Subscript
+
+/**
+ *  To mark both String and Int can be used in subscript.
+ */
+public enum JSONKey {
+    case index(Int)
+    case key(String)
+}
+
+public protocol JSONSubscriptType {
+    var jsonKey: JSONKey { get }
+}
+
+extension Int: JSONSubscriptType {
+    public var jsonKey: JSONKey {
+        return JSONKey.index(self)
+    }
+}
+
+extension String: JSONSubscriptType {
+    public var jsonKey: JSONKey {
+        return JSONKey.key(self)
+    }
+}
+
+extension JSON {
+
+    /// If `type` is `.array`, return json whose object is `array[index]`, otherwise return null json with error.
+    fileprivate subscript(index index: Int) -> JSON {
+        get {
+            if type != .array {
+                var r = JSON.null
+                r.error = self.error ?? SwiftyJSONError.wrongType
+                return r
+            } else if rawArray.indices.contains(index) {
+                return JSON(rawArray[index])
+            } else {
+                var r = JSON.null
+                r.error = SwiftyJSONError.indexOutOfBounds
+                return r
+            }
+        }
+        set {
+            if type == .array &&
+                rawArray.indices.contains(index) &&
+                newValue.error == nil {
+                rawArray[index] = newValue.object
+            }
+        }
+    }
+
+    /// If `type` is `.dictionary`, return json whose object is `dictionary[key]` , otherwise return null json with error.
+    fileprivate subscript(key key: String) -> JSON {
+        get {
+            var r = JSON.null
+            if type == .dictionary {
+                if let o = rawDictionary[key] {
+                    r = JSON(o)
+                } else {
+                    r.error = SwiftyJSONError.notExist
+                }
+            } else {
+                r.error = self.error ?? SwiftyJSONError.wrongType
+            }
+            return r
+        }
+        set {
+            if type == .dictionary && newValue.error == nil {
+                rawDictionary[key] = newValue.object
+            }
+        }
+    }
+
+    /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`,  return `subscript(key:)`.
+    fileprivate subscript(sub sub: JSONSubscriptType) -> JSON {
+        get {
+            switch sub.jsonKey {
+            case .index(let index): return self[index: index]
+            case .key(let key):     return self[key: key]
+            }
+        }
+        set {
+            switch sub.jsonKey {
+            case .index(let index): self[index: index] = newValue
+            case .key(let key):     self[key: key] = newValue
+            }
+        }
+    }
+
+    /**
+     Find a json in the complex data structures by using array of Int and/or String as path.
+    
+     Example:
+    
+     ```
+     let json = JSON[data]
+     let path = [9,"list","person","name"]
+     let name = json[path]
+     ```
+    
+     The same as: let name = json[9]["list"]["person"]["name"]
+    
+     - parameter path: The target json's path.
+    
+     - returns: Return a json found by the path or a null json with error
+     */
+    public subscript(path: [JSONSubscriptType]) -> JSON {
+        get {
+            return path.reduce(self) { $0[sub: $1] }
+        }
+        set {
+            switch path.count {
+            case 0: return
+            case 1: self[sub:path[0]].object = newValue.object
+            default:
+                var aPath = path
+                aPath.remove(at: 0)
+                var nextJSON = self[sub: path[0]]
+                nextJSON[aPath] = newValue
+                self[sub: path[0]] = nextJSON
+            }
+        }
+    }
+
+    /**
+     Find a json in the complex data structures by using array of Int and/or String as path.
+
+     - parameter path: The target json's path. Example:
+
+     let name = json[9,"list","person","name"]
+
+     The same as: let name = json[9]["list"]["person"]["name"]
+
+     - returns: Return a json found by the path or a null json with error
+     */
+    public subscript(path: JSONSubscriptType...) -> JSON {
+        get {
+            return self[path]
+        }
+        set {
+            self[path] = newValue
+        }
+    }
+}
+
+// MARK: - LiteralConvertible
+
+extension JSON: Swift.ExpressibleByStringLiteral {
+
+    public init(stringLiteral value: StringLiteralType) {
+        self.init(value)
+    }
+
+    public init(extendedGraphemeClusterLiteral value: StringLiteralType) {
+        self.init(value)
+    }
+
+    public init(unicodeScalarLiteral value: StringLiteralType) {
+        self.init(value)
+    }
+}
+
+extension JSON: Swift.ExpressibleByIntegerLiteral {
+
+    public init(integerLiteral value: IntegerLiteralType) {
+        self.init(value)
+    }
+}
+
+extension JSON: Swift.ExpressibleByBooleanLiteral {
+
+    public init(booleanLiteral value: BooleanLiteralType) {
+        self.init(value)
+    }
+}
+
+extension JSON: Swift.ExpressibleByFloatLiteral {
+
+    public init(floatLiteral value: FloatLiteralType) {
+        self.init(value)
+    }
+}
+
+extension JSON: Swift.ExpressibleByDictionaryLiteral {
+    public init(dictionaryLiteral elements: (String, Any)...) {
+        let dictionary = elements.reduce(into: [String: Any](), { $0[$1.0] = $1.1})
+        self.init(dictionary)
+    }
+}
+
+extension JSON: Swift.ExpressibleByArrayLiteral {
+
+    public init(arrayLiteral elements: Any...) {
+        self.init(elements)
+    }
+}
+
+// MARK: - Raw
+
+extension JSON: Swift.RawRepresentable {
+
+    public init?(rawValue: Any) {
+        if JSON(rawValue).type == .unknown {
+            return nil
+        } else {
+            self.init(rawValue)
+        }
+    }
+
+    public var rawValue: Any {
+        return object
+    }
+
+    public func rawData(options opt: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0)) throws -> Data {
+        guard JSONSerialization.isValidJSONObject(object) else {
+            throw SwiftyJSONError.invalidJSON
+        }
+
+        return try JSONSerialization.data(withJSONObject: object, options: opt)
+    }
+
+    public func rawString(_ encoding: String.Encoding = .utf8, options opt: JSONSerialization.WritingOptions = .prettyPrinted) -> String? {
+        do {
+            return try _rawString(encoding, options: [.jsonSerialization: opt])
+        } catch {
+            print("Could not serialize object to JSON because:", error.localizedDescription)
+            return nil
+        }
+    }
+
+    public func rawString(_ options: [writingOptionsKeys: Any]) -> String? {
+        let encoding = options[.encoding] as? String.Encoding ?? String.Encoding.utf8
+        let maxObjectDepth = options[.maxObjextDepth] as? Int ?? 10
+        do {
+            return try _rawString(encoding, options: options, maxObjectDepth: maxObjectDepth)
+        } catch {
+            print("Could not serialize object to JSON because:", error.localizedDescription)
+            return nil
+        }
+    }
+
+    fileprivate func _rawString(_ encoding: String.Encoding = .utf8, options: [writingOptionsKeys: Any], maxObjectDepth: Int = 10) throws -> String? {
+        guard maxObjectDepth > 0 else { throw SwiftyJSONError.invalidJSON }
+        switch type {
+        case .dictionary:
+            do {
+                if !(options[.castNilToNSNull] as? Bool ?? false) {
+                    let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted
+                    let data = try rawData(options: jsonOption)
+                    return String(data: data, encoding: encoding)
+                }
+
+                guard let dict = object as? [String: Any?] else {
+                    return nil
+                }
+                let body = try dict.keys.map { key throws -> String in
+                    guard let value = dict[key] else {
+                        return "\"\(key)\": null"
+                    }
+                    guard let unwrappedValue = value else {
+                        return "\"\(key)\": null"
+                    }
+
+                    let nestedValue = JSON(unwrappedValue)
+                    guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else {
+                        throw SwiftyJSONError.elementTooDeep
+                    }
+                    if nestedValue.type == .string {
+                        return "\"\(key)\": \"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
+                    } else {
+                        return "\"\(key)\": \(nestedString)"
+                    }
+                }
+
+                return "{\(body.joined(separator: ","))}"
+            } catch _ {
+                return nil
+            }
+        case .array:
+            do {
+                if !(options[.castNilToNSNull] as? Bool ?? false) {
+                    let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted
+                    let data = try rawData(options: jsonOption)
+                    return String(data: data, encoding: encoding)
+                }
+
+                guard let array = object as? [Any?] else {
+                    return nil
+                }
+                let body = try array.map { value throws -> String in
+                    guard let unwrappedValue = value else {
+                        return "null"
+                    }
+
+                    let nestedValue = JSON(unwrappedValue)
+                    guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else {
+                        throw SwiftyJSONError.invalidJSON
+                    }
+                    if nestedValue.type == .string {
+                        return "\"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
+                    } else {
+                        return nestedString
+                    }
+                }
+
+                return "[\(body.joined(separator: ","))]"
+            } catch _ {
+                return nil
+            }
+        case .string: return rawString
+        case .number: return rawNumber.stringValue
+        case .bool:   return rawBool.description
+        case .null:   return "null"
+        default:      return nil
+        }
+    }
+}
+
+// MARK: - Printable, DebugPrintable
+
+extension JSON: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible {
+
+    public var description: String {
+        return rawString(options: .prettyPrinted) ?? "unknown"
+    }
+
+    public var debugDescription: String {
+        return description
+    }
+}
+
+// MARK: - Array
+
+extension JSON {
+
+    //Optional [JSON]
+    public var array: [JSON]? {
+        return type == .array ? rawArray.map { JSON($0) } : nil
+    }
+
+    //Non-optional [JSON]
+    public var arrayValue: [JSON] {
+        return self.array ?? []
+    }
+
+    //Optional [Any]
+    public var arrayObject: [Any]? {
+        get {
+            switch type {
+            case .array: return rawArray
+            default:     return nil
+            }
+        }
+        set {
+            self.object = newValue ?? NSNull()
+        }
+    }
+}
+
+// MARK: - Dictionary
+
+extension JSON {
+
+    //Optional [String : JSON]
+    public var dictionary: [String: JSON]? {
+        if type == .dictionary {
+            var d = [String: JSON](minimumCapacity: rawDictionary.count)
+            rawDictionary.forEach { pair in
+                d[pair.key] = JSON(pair.value)
+            }
+            return d
+        } else {
+            return nil
+        }
+    }
+
+    //Non-optional [String : JSON]
+    public var dictionaryValue: [String: JSON] {
+        return dictionary ?? [:]
+    }
+
+    //Optional [String : Any]
+
+    public var dictionaryObject: [String: Any]? {
+        get {
+            switch type {
+            case .dictionary: return rawDictionary
+            default:          return nil
+            }
+        }
+        set {
+            object = newValue ?? NSNull()
+        }
+    }
+}
+
+// MARK: - Bool
+
+extension JSON { // : Swift.Bool
+
+    //Optional bool
+    public var bool: Bool? {
+        get {
+            switch type {
+            case .bool: return rawBool
+            default:    return nil
+            }
+        }
+        set {
+            object = newValue ?? NSNull()
+        }
+    }
+
+    //Non-optional bool
+    public var boolValue: Bool {
+        get {
+            switch type {
+            case .bool:   return rawBool
+            case .number: return rawNumber.boolValue
+            case .string: return ["true", "y", "t", "yes", "1"].contains { rawString.caseInsensitiveCompare($0) == .orderedSame }
+            default:      return false
+            }
+        }
+        set {
+            object = newValue
+        }
+    }
+}
+
+// MARK: - String
+
+extension JSON {
+
+    //Optional string
+    public var string: String? {
+        get {
+            switch type {
+            case .string: return object as? String
+            default:      return nil
+            }
+        }
+        set {
+            object = newValue ?? NSNull()
+        }
+    }
+
+    //Non-optional string
+    public var stringValue: String {
+        get {
+            switch type {
+            case .string: return object as? String ?? ""
+            case .number: return rawNumber.stringValue
+            case .bool:   return (object as? Bool).map { String($0) } ?? ""
+            default:      return ""
+            }
+        }
+        set {
+            object = newValue
+        }
+    }
+}
+
+// MARK: - Number
+
+extension JSON {
+
+    //Optional number
+    public var number: NSNumber? {
+        get {
+            switch type {
+            case .number: return rawNumber
+            case .bool:   return NSNumber(value: rawBool ? 1 : 0)
+            default:      return nil
+            }
+        }
+        set {
+            object = newValue ?? NSNull()
+        }
+    }
+
+    //Non-optional number
+    public var numberValue: NSNumber {
+        get {
+            switch type {
+            case .string:
+                let decimal = NSDecimalNumber(string: object as? String)
+                return decimal == .notANumber ? .zero : decimal
+            case .number: return object as? NSNumber ?? NSNumber(value: 0)
+            case .bool: return NSNumber(value: rawBool ? 1 : 0)
+            default: return NSNumber(value: 0.0)
+            }
+        }
+        set {
+            object = newValue
+        }
+    }
+}
+
+// MARK: - Null
+
+extension JSON {
+
+    public var null: NSNull? {
+        set {
+            object = NSNull()
+        }
+        get {
+            switch type {
+            case .null: return rawNull
+            default:    return nil
+            }
+        }
+    }
+    public func exists() -> Bool {
+        if let errorValue = error, (400...1000).contains(errorValue.errorCode) {
+            return false
+        }
+        return true
+    }
+}
+
+// MARK: - URL
+
+extension JSON {
+
+    //Optional URL
+    public var url: URL? {
+        get {
+            switch type {
+            case .string:
+                // Check for existing percent escapes first to prevent double-escaping of % character
+                if rawString.range(of: "%[0-9A-Fa-f]{2}", options: .regularExpression, range: nil, locale: nil) != nil {
+                    return Foundation.URL(string: rawString)
+                } else if let encodedString_ = rawString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) {
+                    // We have to use `Foundation.URL` otherwise it conflicts with the variable name.
+                    return Foundation.URL(string: encodedString_)
+                } else {
+                    return nil
+                }
+            default:
+                return nil
+            }
+        }
+        set {
+            object = newValue?.absoluteString ?? NSNull()
+        }
+    }
+}
+
+// MARK: - Int, Double, Float, Int8, Int16, Int32, Int64
+
+extension JSON {
+
+    public var double: Double? {
+        get {
+            return number?.doubleValue
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object = NSNull()
+            }
+        }
+    }
+
+    public var doubleValue: Double {
+        get {
+            return numberValue.doubleValue
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var float: Float? {
+        get {
+            return number?.floatValue
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object = NSNull()
+            }
+        }
+    }
+
+    public var floatValue: Float {
+        get {
+            return numberValue.floatValue
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var int: Int? {
+        get {
+            return number?.intValue
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object = NSNull()
+            }
+        }
+    }
+
+    public var intValue: Int {
+        get {
+            return numberValue.intValue
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var uInt: UInt? {
+        get {
+            return number?.uintValue
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object = NSNull()
+            }
+        }
+    }
+
+    public var uIntValue: UInt {
+        get {
+            return numberValue.uintValue
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var int8: Int8? {
+        get {
+            return number?.int8Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: Int(newValue))
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var int8Value: Int8 {
+        get {
+            return numberValue.int8Value
+        }
+        set {
+            object = NSNumber(value: Int(newValue))
+        }
+    }
+
+    public var uInt8: UInt8? {
+        get {
+            return number?.uint8Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var uInt8Value: UInt8 {
+        get {
+            return numberValue.uint8Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var int16: Int16? {
+        get {
+            return number?.int16Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var int16Value: Int16 {
+        get {
+            return numberValue.int16Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var uInt16: UInt16? {
+        get {
+            return number?.uint16Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var uInt16Value: UInt16 {
+        get {
+            return numberValue.uint16Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var int32: Int32? {
+        get {
+            return number?.int32Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var int32Value: Int32 {
+        get {
+            return numberValue.int32Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var uInt32: UInt32? {
+        get {
+            return number?.uint32Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var uInt32Value: UInt32 {
+        get {
+            return numberValue.uint32Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var int64: Int64? {
+        get {
+            return number?.int64Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var int64Value: Int64 {
+        get {
+            return numberValue.int64Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+
+    public var uInt64: UInt64? {
+        get {
+            return number?.uint64Value
+        }
+        set {
+            if let newValue = newValue {
+                object = NSNumber(value: newValue)
+            } else {
+                object =  NSNull()
+            }
+        }
+    }
+
+    public var uInt64Value: UInt64 {
+        get {
+            return numberValue.uint64Value
+        }
+        set {
+            object = NSNumber(value: newValue)
+        }
+    }
+}
+
+// MARK: - Comparable
+
+extension JSON: Swift.Comparable {}
+
+public func == (lhs: JSON, rhs: JSON) -> Bool {
+
+    switch (lhs.type, rhs.type) {
+    case (.number, .number): return lhs.rawNumber == rhs.rawNumber
+    case (.string, .string): return lhs.rawString == rhs.rawString
+    case (.bool, .bool):     return lhs.rawBool == rhs.rawBool
+    case (.array, .array):   return lhs.rawArray as NSArray == rhs.rawArray as NSArray
+    case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
+    case (.null, .null):     return true
+    default:                 return false
+    }
+}
+
+public func <= (lhs: JSON, rhs: JSON) -> Bool {
+
+    switch (lhs.type, rhs.type) {
+    case (.number, .number): return lhs.rawNumber <= rhs.rawNumber
+    case (.string, .string): return lhs.rawString <= rhs.rawString
+    case (.bool, .bool):     return lhs.rawBool == rhs.rawBool
+    case (.array, .array):   return lhs.rawArray as NSArray == rhs.rawArray as NSArray
+    case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
+    case (.null, .null):     return true
+    default:                 return false
+    }
+}
+
+public func >= (lhs: JSON, rhs: JSON) -> Bool {
+
+    switch (lhs.type, rhs.type) {
+    case (.number, .number): return lhs.rawNumber >= rhs.rawNumber
+    case (.string, .string): return lhs.rawString >= rhs.rawString
+    case (.bool, .bool):     return lhs.rawBool == rhs.rawBool
+    case (.array, .array):   return lhs.rawArray as NSArray == rhs.rawArray as NSArray
+    case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
+    case (.null, .null):     return true
+    default:                 return false
+    }
+}
+
+public func > (lhs: JSON, rhs: JSON) -> Bool {
+
+    switch (lhs.type, rhs.type) {
+    case (.number, .number): return lhs.rawNumber > rhs.rawNumber
+    case (.string, .string): return lhs.rawString > rhs.rawString
+    default:                 return false
+    }
+}
+
+public func < (lhs: JSON, rhs: JSON) -> Bool {
+
+    switch (lhs.type, rhs.type) {
+    case (.number, .number): return lhs.rawNumber < rhs.rawNumber
+    case (.string, .string): return lhs.rawString < rhs.rawString
+    default:                 return false
+    }
+}
+
+private let trueNumber = NSNumber(value: true)
+private let falseNumber = NSNumber(value: false)
+private let trueObjCType = String(cString: trueNumber.objCType)
+private let falseObjCType = String(cString: falseNumber.objCType)
+
+// MARK: - NSNumber: Comparable
+
+extension NSNumber {
+    fileprivate var isBool: Bool {
+        let objCType = String(cString: self.objCType)
+        if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) {
+            return true
+        } else {
+            return false
+        }
+    }
+}
+
+func == (lhs: NSNumber, rhs: NSNumber) -> Bool {
+    switch (lhs.isBool, rhs.isBool) {
+    case (false, true): return false
+    case (true, false): return false
+    default:            return lhs.compare(rhs) == .orderedSame
+    }
+}
+
+func != (lhs: NSNumber, rhs: NSNumber) -> Bool {
+    return !(lhs == rhs)
+}
+
+func < (lhs: NSNumber, rhs: NSNumber) -> Bool {
+
+    switch (lhs.isBool, rhs.isBool) {
+    case (false, true): return false
+    case (true, false): return false
+    default:            return lhs.compare(rhs) == .orderedAscending
+    }
+}
+
+func > (lhs: NSNumber, rhs: NSNumber) -> Bool {
+
+    switch (lhs.isBool, rhs.isBool) {
+    case (false, true): return false
+    case (true, false): return false
+    default:            return lhs.compare(rhs) == ComparisonResult.orderedDescending
+    }
+}
+
+func <= (lhs: NSNumber, rhs: NSNumber) -> Bool {
+
+    switch (lhs.isBool, rhs.isBool) {
+    case (false, true): return false
+    case (true, false): return false
+    default:            return lhs.compare(rhs) != .orderedDescending
+    }
+}
+
+func >= (lhs: NSNumber, rhs: NSNumber) -> Bool {
+
+    switch (lhs.isBool, rhs.isBool) {
+    case (false, true): return false
+    case (true, false): return false
+    default:            return lhs.compare(rhs) != .orderedAscending
+    }
+}
+
+public enum writingOptionsKeys {
+    case jsonSerialization
+    case castNilToNSNull
+    case maxObjextDepth
+    case encoding
+}
+
+// MARK: - JSON: Codable
+extension JSON: Codable {
+    private static var codableTypes: [Codable.Type] {
+        return [
+            Bool.self,
+            Int.self,
+            Int8.self,
+            Int16.self,
+            Int32.self,
+            Int64.self,
+            UInt.self,
+            UInt8.self,
+            UInt16.self,
+            UInt32.self,
+            UInt64.self,
+            Double.self,
+            String.self,
+            [JSON].self,
+            [String: JSON].self
+        ]
+    }
+    public init(from decoder: Decoder) throws {
+        var object: Any?
+
+        if let container = try? decoder.singleValueContainer(), !container.decodeNil() {
+            for type in JSON.codableTypes {
+                if object != nil {
+                    break
+                }
+                // try to decode value
+                switch type {
+                case let boolType as Bool.Type:
+                    object = try? container.decode(boolType)
+                case let intType as Int.Type:
+                    object = try? container.decode(intType)
+                case let int8Type as Int8.Type:
+                    object = try? container.decode(int8Type)
+                case let int32Type as Int32.Type:
+                    object = try? container.decode(int32Type)
+                case let int64Type as Int64.Type:
+                    object = try? container.decode(int64Type)
+                case let uintType as UInt.Type:
+                    object = try? container.decode(uintType)
+                case let uint8Type as UInt8.Type:
+                    object = try? container.decode(uint8Type)
+                case let uint16Type as UInt16.Type:
+                    object = try? container.decode(uint16Type)
+                case let uint32Type as UInt32.Type:
+                    object = try? container.decode(uint32Type)
+                case let uint64Type as UInt64.Type:
+                    object = try? container.decode(uint64Type)
+                case let doubleType as Double.Type:
+                    object = try? container.decode(doubleType)
+                case let stringType as String.Type:
+                    object = try? container.decode(stringType)
+                case let jsonValueArrayType as [JSON].Type:
+                    object = try? container.decode(jsonValueArrayType)
+                case let jsonValueDictType as [String: JSON].Type:
+                    object = try? container.decode(jsonValueDictType)
+                default:
+                    break
+                }
+            }
+        }
+        self.init(object ?? NSNull())
+    }
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.singleValueContainer()
+        if object is NSNull {
+            try container.encodeNil()
+            return
+        }
+        switch object {
+        case let intValue as Int:
+            try container.encode(intValue)
+        case let int8Value as Int8:
+            try container.encode(int8Value)
+        case let int32Value as Int32:
+            try container.encode(int32Value)
+        case let int64Value as Int64:
+            try container.encode(int64Value)
+        case let uintValue as UInt:
+            try container.encode(uintValue)
+        case let uint8Value as UInt8:
+            try container.encode(uint8Value)
+        case let uint16Value as UInt16:
+            try container.encode(uint16Value)
+        case let uint32Value as UInt32:
+            try container.encode(uint32Value)
+        case let uint64Value as UInt64:
+            try container.encode(uint64Value)
+        case let doubleValue as Double:
+            try container.encode(doubleValue)
+        case let boolValue as Bool:
+            try container.encode(boolValue)
+        case let stringValue as String:
+            try container.encode(stringValue)
+        case is [Any]:
+            let jsonValueArray = array ?? []
+            try container.encode(jsonValueArray)
+        case is [String: Any]:
+            let jsonValueDictValue = dictionary ?? [:]
+            try container.encode(jsonValueDictValue)
+        default:
+            break
+        }
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift
new file mode 100644
index 000000000..1f7f7968d
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift
@@ -0,0 +1,183 @@
+import Foundation
+import ErgoLibC
+
+/// Token id (32 byte digest)
+class TokenId {
+    internal var pointer: TokenIdPtr
+    
+    /// Create token id from ergo box id (32 byte digest)
+    init(fromBoxId : BoxId) {
+        var ptr: TokenIdPtr?
+        ergo_lib_token_id_from_box_id(fromBoxId.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``TokenIdPtr``. Note: we must ensure that no other instance
+    /// of ``TokenId`` can hold this pointer.
+    init(withRawPointer ptr: TokenIdPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Parse token id (32 byte digest) from base16-encoded string
+    init(fromBase16EncodedString : String) throws {
+        var ptr: TokenIdPtr?
+        let error = fromBase16EncodedString.withCString { cs in
+            ergo_lib_token_id_from_str(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Get base16 encoded string
+    func toBase16EncodedString() -> String {
+        var cStr: UnsafePointer<CChar>?
+        ergo_lib_token_id_to_str(self.pointer, &cStr)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return str
+    }
+    
+    deinit {
+        ergo_lib_token_id_delete(self.pointer)
+    }
+}
+
+extension TokenId: Equatable {
+    static func ==(lhs: TokenId, rhs: TokenId) -> Bool {
+        ergo_lib_token_id_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Token amount with bound checks
+class TokenAmount {
+    internal var pointer: TokenAmountPtr
+    
+    /// Create instance from ``Int64`` with bounds check
+    init(fromInt64 : Int64) throws {
+        var ptr: TokenAmountPtr?
+        let error = ergo_lib_token_amount_from_i64(fromInt64, &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``TokenAmountPtr``. Note: we must ensure that no other instance
+    /// of ``TokenAmount`` can hold this pointer.
+    init(withRawPointer ptr: TokenAmountPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get value as ``Int64``
+    func toInt64() -> Int64 {
+        return ergo_lib_token_amount_as_i64(self.pointer)
+    }
+    
+    deinit {
+        ergo_lib_token_amount_delete(self.pointer)
+    }
+}
+
+extension TokenAmount: Equatable {
+    static func ==(lhs: TokenAmount, rhs: TokenAmount) -> Bool {
+        ergo_lib_token_amount_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// Token represented with token id paired with its amount
+class Token {
+    internal var pointer: TokenPtr
+    
+    /// Create a token with given token id and amount
+    init(tokenId : TokenId, tokenAmount: TokenAmount) {
+        var ptr: TokenPtr?
+        ergo_lib_token_new(tokenId.pointer, tokenAmount.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``TokenPtr``. Note: we must ensure that no other instance
+    /// of ``Token`` can hold this pointer.
+    init(withRawPointer ptr: TokenPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get token id
+    func getId() -> TokenId {
+        var tokenIdPtr: TokenIdPtr?
+        ergo_lib_token_get_id(self.pointer, &tokenIdPtr)
+        return TokenId(withRawPointer: tokenIdPtr!)
+    }
+    
+    /// Get token amount
+    func getAmount() -> TokenAmount {
+        var tokenAmountPtr: TokenAmountPtr?
+        ergo_lib_token_get_amount(self.pointer, &tokenAmountPtr)
+        return TokenAmount(withRawPointer: tokenAmountPtr!)
+    }
+    
+    /// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+    func toJsonEIP12() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_token_to_json_eip12(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    deinit {
+        ergo_lib_token_delete(self.pointer)
+    }
+}
+
+extension Token: Equatable {
+    static func ==(lhs: Token, rhs: Token) -> Bool {
+        ergo_lib_token_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
+/// An ordered collection of ``Token``s
+class Tokens {
+    internal var pointer: TokensPtr
+    
+    /// Create an empty collection
+    init() {
+        var tokensPtr: TokensPtr?
+        ergo_lib_tokens_new(&tokensPtr)
+        self.pointer = tokensPtr!
+    }
+    
+    /// Takes ownership of an existing ``TokensPtr``. Note: we must ensure that no other instance
+    /// of ``Tokens`` can hold this pointer.
+    init(withPtr ptr: TokensPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_tokens_len(self.pointer)
+    }
+    
+    /// Returns the ``Token`` at location `index` if it exists.
+    func get(index: UInt) -> Token? {
+        var tokenPtr: TokenPtr?
+        let res = ergo_lib_tokens_get(self.pointer, index, &tokenPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return Token(withRawPointer: tokenPtr!)
+        } else {
+            return nil
+        }
+    }
+    
+    /// Add a ``Token`` to the end of the collection. Note that the collection has a maximum
+    /// capacity of 255 tokens. Will throw error if adding more.
+    func add(token: Token) throws {
+        let error = ergo_lib_tokens_add(token.pointer, self.pointer)
+        try checkError(error)
+    }
+        
+    deinit {
+        ergo_lib_tokens_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift
new file mode 100644
index 000000000..33ac2c7a0
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Transaction.swift
@@ -0,0 +1,219 @@
+
+import Foundation
+import ErgoLibC
+
+/// Unsigned (inputs without proofs) transaction
+class UnsignedTransaction {
+    internal var pointer: UnsignedTransactionPtr
+    
+    /// Parse from JSON. Supports Ergo Node/Explorer API and box values and token amount encoded as
+    /// strings
+    init(withJson json: String) throws {
+        var ptr: UnsignedTransactionPtr?
+        let error = json.withCString { cs in
+            ergo_lib_unsigned_tx_from_json(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``UnsignedTransactionPtr``. Note: we must ensure that no other instance
+    /// of ``UnsignedTransaction`` can hold this pointer.
+    internal init(withRawPointer ptr: UnsignedTransactionPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get ``TxId`` of this unsigned transaction
+    func getTxId() -> TxId {
+        var ptr: TxIdPtr?
+        ergo_lib_unsigned_tx_id(self.pointer, &ptr)
+        return TxId(withRawPointer: ptr!)
+    }
+    
+    /// Get ``UnsignedInputs`` for this unsigned transaction
+    func getUnsignedInputs() -> UnsignedInputs {
+        var unsignedInputsPtr: UnsignedInputsPtr?
+        ergo_lib_unsigned_tx_inputs(self.pointer, &unsignedInputsPtr)
+        return UnsignedInputs(withRawPointer: unsignedInputsPtr!)
+    }
+    
+    /// Get ``DataInputs`` for this unsigned transaction
+    func getDataInputs() -> DataInputs {
+        var dataInputsPtr: DataInputsPtr?
+        ergo_lib_unsigned_tx_data_inputs(self.pointer, &dataInputsPtr)
+        return DataInputs(withRawPointer: dataInputsPtr!)
+    }
+    
+    /// Get output candidates for this unsigned transaction
+    func getOutputCandidates() -> ErgoBoxCandidates {
+        var ptr: ErgoBoxCandidatesPtr?
+        ergo_lib_unsigned_tx_output_candidates(self.pointer, &ptr)
+        return ErgoBoxCandidates(withRawPointer: ptr!)
+    }
+    
+    /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+    func toJSON() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_unsigned_tx_to_json(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    /// JSON representation according to EIP-12 https://github.com/ergoplatform/eips/pull/23
+    func toJsonEIP12() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_unsigned_tx_to_json_eip12(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    deinit {
+        ergo_lib_unsigned_tx_delete(self.pointer)
+    }
+}
+
+/**
+ * ErgoTransaction is an atomic state transition operation. It destroys Boxes from the state
+ * and creates new ones. If transaction is spending boxes protected by some non-trivial scripts,
+ * its inputs should also contain proof of spending correctness - context extension (user-defined
+ * key-value map) and data inputs (links to existing boxes in the state) that may be used during
+ * script reduction to crypto, signatures that satisfies the remaining cryptographic protection
+ * of the script.
+ * Transactions are not encrypted, so it is possible to browse and view every transaction ever
+ * collected into a block.
+ */
+class Transaction {
+    internal var pointer: TransactionPtr
+    
+    /// Create ``Transaction`` from ``UnsignedTransaction`` and an array of proofs in the same order
+    /// as `UnsignedTransaction.inputs` with empty proof indicated with empty byte array
+    init(unsignedTx: UnsignedTransaction, proofs: ByteArrays) throws {
+        var ptr: TransactionPtr?
+        let error = ergo_lib_tx_from_unsigned_tx(unsignedTx.pointer, proofs.pointer, &ptr)
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Parse from JSON. Supports Ergo Node/Explorer API and box values and token amount encoded as
+    /// strings
+    init(withJson json: String) throws {
+        var ptr: TransactionPtr?
+        let error = json.withCString { cs in
+            ergo_lib_tx_from_json(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``TransactionPtr``. Note: we must ensure that no other instance
+    /// of ``Transaction`` can hold this pointer.
+    internal init(withRawPointer ptr: TransactionPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get ``TxId`` of this transaction
+    func getTxId() -> TxId {
+        var ptr: TxIdPtr?
+        ergo_lib_tx_id(self.pointer, &ptr)
+        return TxId(withRawPointer: ptr!)
+    }
+    
+    /// Get ``Inputs`` for this transaction
+    func getInputs() -> Inputs {
+        var ptr: UnsignedInputsPtr?
+        ergo_lib_tx_inputs(self.pointer, &ptr)
+        return Inputs(withRawPointer: ptr!)
+    }
+    
+    /// Get ``DataInputs`` for this transaction
+    func getDataInputs() -> DataInputs {
+        var ptr: DataInputsPtr?
+        ergo_lib_tx_data_inputs(self.pointer, &ptr)
+        return DataInputs(withRawPointer: ptr!)
+    }
+    
+    /// Get output candidates for this transaction
+    func getOutputCandidates() -> ErgoBoxCandidates {
+        var ptr: ErgoBoxCandidatesPtr?
+        ergo_lib_tx_output_candidates(self.pointer, &ptr)
+        return ErgoBoxCandidates(withRawPointer: ptr!)
+    }
+    
+    /// Returns ``ErgoBoxes`` created from ``ErgoBoxCandidate``'s with tx id and indices
+    func getOutputs() -> ErgoBoxes {
+        var ptr: ErgoBoxesPtr?
+        ergo_lib_tx_outputs(self.pointer, &ptr)
+        return ErgoBoxes(withRawPointer: ptr!)
+    }
+    
+    /// JSON representation as text (compatible with Ergo Node/Explorer API, numbers are encoded as numbers)
+    func toJSON() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_tx_to_json(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    /// JSON representation according to EIP-12 <https://github.com/ergoplatform/eips/pull/23>
+    func toJsonEIP12() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_tx_to_json_eip12(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+    
+    deinit {
+        ergo_lib_tx_delete(self.pointer)
+    }
+}
+
+/// Transaction id
+class TxId {
+    internal var pointer: TxIdPtr
+    
+    /// Create instance from hex-encoded string
+    init(withString str: String) throws {
+        var ptr: TxIdPtr?
+        let error = str.withCString { cs in
+            ergo_lib_tx_id_from_str(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Takes ownership of an existing ``TxIdPtr``. Note: we must ensure that no other instance
+    /// of ``TxId`` can hold this pointer.
+    internal init(withRawPointer ptr: TxIdPtr) {
+        self.pointer = ptr
+    }
+    
+    /// Get the tx id as bytes
+    func toString() throws -> String {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_tx_id_to_str(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return str
+    }
+    
+    deinit {
+        ergo_lib_tx_id_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift
new file mode 100644
index 000000000..cd1c3dc01
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/TxBuilder.swift
@@ -0,0 +1,108 @@
+import Foundation
+import ErgoLibC
+
+/// ``UnsignedTransaction`` builder
+class TxBuilder {
+    internal var pointer: TxBuilderPtr
+    
+    /// Creates new ``TxBuilder``
+    /// - Parameters
+    ///  - `boxSelection`: selected input boxes
+    ///  - `outputCandidates` - output boxes to be "created" in this transaction,
+    ///  - `currentHeight`: chain height that will be used in additionally created boxes (change, miner's fee, etc.),
+    ///  - `feeAmount`: miner's fee,
+    ///  - `changeAddress`: change (inputs - outputs) will be sent to this address,
+    ///  - `minChangeValue`: minimal value of the change to be sent to `change_address`, value less than that
+    ///     will be given to miners,
+    init(
+        boxSelection : BoxSelection,
+        outputCandidates: ErgoBoxCandidates,
+        currentHeight: UInt32,
+        feeAmount: BoxValue,
+        changeAddress: Address,
+        minChangeValue: BoxValue
+    ) {
+        var ptr: TxBuilderPtr?
+        ergo_lib_tx_builder_new(
+            boxSelection.pointer,
+            outputCandidates.pointer,
+            currentHeight,
+            feeAmount.pointer,
+            changeAddress.pointer,
+            minChangeValue.pointer,
+            &ptr
+        )
+        self.pointer = ptr!
+    }
+    
+    /// Suggested transaction fee (semi-default value used across wallets and dApps as of Oct 2020)
+    static func SUGGESTED_TX_FEE() -> BoxValue {
+        var ptr: BoxValuePtr?
+        ergo_lib_tx_builder_suggested_tx_fee(&ptr)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    /// Set transaction's ``DataInputs``
+    func setDataInputs(dataInputs: DataInputs) {
+        ergo_lib_tx_builder_set_data_inputs(self.pointer, dataInputs.pointer)
+    }
+    
+    /// Get ``DataInputs``
+    func getDataInputs() -> DataInputs {
+        var ptr: DataInputsPtr?
+        ergo_lib_tx_builder_data_inputs(self.pointer, &ptr)
+        return DataInputs(withRawPointer: ptr!)
+    }
+    
+    /// Build the ``UnsignedTransaction``
+    func build() throws -> UnsignedTransaction {
+        var ptr: UnsignedTransactionPtr?
+        let error = ergo_lib_tx_builder_build(self.pointer, &ptr)
+        try checkError(error)
+        return UnsignedTransaction(withRawPointer: ptr!)
+    }
+    
+    /// Get ``BoxSelection``
+    func getBoxSelection() -> BoxSelection {
+        var ptr: BoxSelectionPtr?
+        ergo_lib_tx_builder_box_selection(self.pointer, &ptr)
+        return BoxSelection(withRawPointer: ptr!)
+    }
+    
+    /// Get ``OutputCandidates``
+    func getOutputCandidates() -> ErgoBoxCandidates {
+        var ptr: ErgoBoxCandidatesPtr?
+        ergo_lib_tx_builder_output_candidates(self.pointer, &ptr)
+        return ErgoBoxCandidates(withRawPointer: ptr!)
+    }
+    
+    /// Get current height
+    func getCurrentHeight() -> UInt32 {
+        return ergo_lib_tx_builder_current_height(self.pointer)
+    }
+    
+    /// Get fee amount
+    func getFeeAmount() -> BoxValue {
+        var ptr: BoxValuePtr?
+        ergo_lib_tx_builder_fee_amount(self.pointer, &ptr)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    /// Get change ``Address``
+    func getChangeAddress() -> Address {
+        var ptr: AddressPtr?
+        ergo_lib_tx_builder_change_address(self.pointer, &ptr)
+        return Address(withRawPointer: ptr!)
+    }
+    
+    /// Get min change value
+    func getMinChangeValue() -> BoxValue {
+        var ptr: BoxValuePtr?
+        ergo_lib_tx_builder_min_change_value(self.pointer, &ptr)
+        return BoxValue(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_tx_builder_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift
new file mode 100644
index 000000000..33e7f89a1
--- /dev/null
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Wallet.swift
@@ -0,0 +1,54 @@
+import Foundation
+import ErgoLibC
+
+class Wallet {
+    internal var pointer: WalletPtr
+    
+    /// Create ``Wallet`` instance loading secret key from mnemonic. Throws error if a DlogSecretKey cannot be
+    /// parsed from the provided phrase
+    init(mnemonicPhrase : String, mnemonicPass: String) throws {
+        var ptr: WalletPtr?
+        let error =
+            mnemonicPhrase.withCString{phrase -> ErrorPtr? in
+                mnemonicPass.withCString{pass in
+                    ergo_lib_wallet_from_mnemonic(phrase, pass, &ptr)
+                }
+            }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+    
+    /// Create ``Wallet`` from secrets
+    init(secrets: SecretKeys) {
+        var ptr: WalletPtr?
+        ergo_lib_wallet_from_secrets(secrets.pointer, &ptr)
+        self.pointer = ptr!
+    }
+    
+    /// Sign a transaction
+    func signTransaction(
+        stateContext: ErgoStateContext,
+        unsignedTx: UnsignedTransaction,
+        boxesToSpend: ErgoBoxes,
+        dataBoxes: ErgoBoxes
+    ) throws -> Transaction {
+        var ptr: TransactionPtr?
+        let error = ergo_lib_wallet_sign_transaction(self.pointer, stateContext.pointer, unsignedTx.pointer, boxesToSpend.pointer, dataBoxes.pointer, &ptr)
+        try checkError(error)
+        return Transaction(withRawPointer: ptr!)
+    }
+    
+    /// Signs a reduced transaction (generating proofs for inputs)
+    func signReducedTransaction(
+        reducedTx: ReducedTransaction
+    ) throws -> Transaction {
+        var ptr: TransactionPtr?
+        let error = ergo_lib_wallet_sign_reduced_transaction(self.pointer, reducedTx.pointer, &ptr)
+        try checkError(error)
+        return Transaction(withRawPointer: ptr!)
+    }
+    
+    deinit {
+        ergo_lib_wallet_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLibC/module.modulemap b/bindings/ergo-lib-ios/Sources/ErgoLibC/module.modulemap
index a2895ce13..ab394dd0c 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLibC/module.modulemap
+++ b/bindings/ergo-lib-ios/Sources/ErgoLibC/module.modulemap
@@ -1,4 +1,5 @@
-module ErgoLibC {
+module ErgoLibC [system] {
     umbrella header "shim.h"
+    link "ergo_lib_c"
     export *
 }
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLibC/shim.h b/bindings/ergo-lib-ios/Sources/ErgoLibC/shim.h
index 207f86805..46d328c66 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLibC/shim.h
+++ b/bindings/ergo-lib-ios/Sources/ErgoLibC/shim.h
@@ -1,6 +1,6 @@
-#ifndef ERGO_WALLET_SHIM_H_
-#define ERGO_WALLET_SHIM_H_
+#ifndef ERGO_LIB_SHIM_H_
+#define ERGO_LIB_SHIM_H_
 
-#include "ergo_wallet.h"
+#include "../../../ergo-lib-c/h/ergo_lib.h"
 
-#endif /* ERGO_WALLET_SHIM_H_ */
+#endif /* ERGO_LIB_SHIM_H_ */
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/AddressTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/AddressTests.swift
new file mode 100644
index 000000000..26ae6b570
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/AddressTests.swift
@@ -0,0 +1,40 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class AddressTests: XCTestCase {
+    func testTestnetAddress() throws {
+        let p2pkAddrStr = "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN"
+        let p2pkAddr = try ErgoLib.Address(withTestnetAddress: p2pkAddrStr)
+        XCTAssertEqual(p2pkAddr.toBase58(networkPrefix: NetworkPrefix.Testnet), p2pkAddrStr)
+        XCTAssertEqual(p2pkAddr.typePrefix(), AddressTypePrefix.P2Pk)
+        
+        let p2shAddrStr = "rbcrmKEYduUvADj9Ts3dSVSG27h54pgrq5fPuwB"
+        let p2shAddr = try ErgoLib.Address(withTestnetAddress: p2shAddrStr)
+        XCTAssertEqual(p2shAddr.toBase58(networkPrefix: NetworkPrefix.Testnet), p2shAddrStr)
+        XCTAssertEqual(p2shAddr.typePrefix(), AddressTypePrefix.Pay2Sh)
+        
+    }
+    
+    func testMainnetAddress() throws {
+        let p2pkAddrStr = "9fRAWhdxEsTcdb8PhGNrZfwqa65zfkuYHAMmkQLcic1gdLSV5vA"
+        let p2pkAddr = try ErgoLib.Address(withMainnetAddress: p2pkAddrStr)
+        XCTAssertEqual(p2pkAddr.toBase58(networkPrefix: NetworkPrefix.Mainnet), p2pkAddrStr)
+        XCTAssertEqual(p2pkAddr.typePrefix(), AddressTypePrefix.P2Pk)
+        
+        let p2shAddrStr = "8UApt8czfFVuTgQmMwtsRBZ4nfWquNiSwCWUjMg"
+        let p2shAddr = try ErgoLib.Address(withMainnetAddress: p2shAddrStr)
+        XCTAssertEqual(p2shAddr.toBase58(networkPrefix: NetworkPrefix.Mainnet), p2shAddrStr)
+        XCTAssertEqual(p2shAddr.typePrefix(), AddressTypePrefix.Pay2Sh)
+    }
+    
+    func testBase58Address() throws {
+        let validMainnetAddrStr = "9fRAWhdxEsTcdb8PhGNrZfwqa65zfkuYHAMmkQLcic1gdLSV5vA"
+        let fromBase58Addr = try ErgoLib.Address(withBase58Address: validMainnetAddrStr)
+        XCTAssertEqual(fromBase58Addr.toBase58(networkPrefix: NetworkPrefix.Mainnet), validMainnetAddrStr)
+    }
+    
+    func testInvalidAddress() throws {
+        XCTAssertThrowsError(try ErgoLib.Address(withTestnetAddress: "sss"))
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/BoxBuilderTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/BoxBuilderTests.swift
new file mode 100644
index 000000000..6526e160c
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/BoxBuilderTests.swift
@@ -0,0 +1,42 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class BoxBuilderTests: XCTestCase {
+    static func genElements() throws -> (Contract, BoxValue) {
+        let addr = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let contract = try Contract(payToAddress: addr)
+        let boxValue = try BoxValue(fromInt64: Int64(10000000))
+        return (contract, boxValue)
+    }
+    
+    func testSimpleBox() throws {
+        let (contract, boxValue) = try BoxBuilderTests.genElements()
+        XCTAssertNoThrow(ErgoBoxCandidateBuilder(boxValue: boxValue, contract: contract, creationHeight: UInt32(0)))
+    }
+    
+    func testSetRegister() throws {
+        let (contract, boxValue) = try BoxBuilderTests.genElements()
+        let constant = Constant(withInt32: Int32(1))
+        let builder = ErgoBoxCandidateBuilder(boxValue: boxValue, contract: contract, creationHeight: UInt32(0))
+            .setRegisterValue(registerId: NonMandatoryRegisterId.R4, constant: constant)
+        XCTAssertEqual(builder.getRegisterValue(registerId: NonMandatoryRegisterId.R4)!, constant)
+        let box = try builder.build()
+        XCTAssertEqual(box.getRegisterValue(registerId: NonMandatoryRegisterId.R4)!, constant)
+    }
+    
+    func testDeleteRegister() throws {
+        let (contract, boxValue) = try BoxBuilderTests.genElements()
+        let constant = Constant(withInt32: Int32(1))
+        var builder = ErgoBoxCandidateBuilder(boxValue: boxValue, contract: contract, creationHeight: UInt32(0))
+            .setRegisterValue(registerId: NonMandatoryRegisterId.R4, constant: constant)
+        XCTAssertEqual(builder.getRegisterValue(registerId: NonMandatoryRegisterId.R4)!, constant)
+        
+        // Now delete register value
+        builder = builder.deleteRegisterValue(registerId: NonMandatoryRegisterId.R4)
+        XCTAssertNil(builder.getRegisterValue(registerId: NonMandatoryRegisterId.R4))
+        let box = try builder.build()
+        XCTAssertNil(box.getRegisterValue(registerId: NonMandatoryRegisterId.R4))
+    }
+}
+
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/BoxSelectorTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/BoxSelectorTests.swift
new file mode 100644
index 000000000..9c1ea6065
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/BoxSelectorTests.swift
@@ -0,0 +1,32 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class BoxSelectorTests: XCTestCase {
+    func test() throws {
+        let jsonStr = """
+        {
+          "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+          "value": 67500000000,
+          "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+          "assets": [],
+          "creationHeight": 284761,
+          "additionalRegisters": {},
+          "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+          "index": 1
+        }
+        """
+        let json = [jsonStr.data(using: .utf8, allowLossyConversion: false)!]
+        let unspentBoxes = try ErgoBoxes(fromJSON: json)
+        let selection = try SimpleBoxSelector()
+            .select(
+                inputs: unspentBoxes,
+                targetBalance: BoxValue(fromInt64: Int64(10000000)),
+                targetTokens: Tokens()
+            )
+        XCTAssertEqual(
+            selection.getBoxes().get(index: UInt(0))!.getBoxId(),
+            unspentBoxes.get(index: UInt(0))!.getBoxId()
+        )
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift
new file mode 100644
index 000000000..ea3a8a643
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ConstantTests.swift
@@ -0,0 +1,69 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class ConstantTests: XCTestCase {
+    func testI32Roundtrip() throws {
+        let c = Constant(withInt32: Int32(999999999))
+        let encoded = try c.toBase16String()
+        let decodedC = try Constant(withBase16Str: encoded)
+        XCTAssertEqual(c, decodedC)
+    }
+    
+    func testI64Roundtrip() throws {
+        let c = Constant(withInt64: Int64(9223372036854775807))
+        let encoded = try c.toBase16String()
+        let decodedC = try Constant(withBase16Str: encoded)
+        XCTAssertEqual(c, decodedC)
+    }
+    
+    func testByteArrayRoundtrip() throws {
+        let arr = [1, 1, 2, 255].map{ UInt8($0)}
+        let c = try Constant(withBytes: arr)
+        let encoded = try c.toBase16String()
+        let decodedC = try Constant(withBase16Str: encoded)
+        XCTAssertEqual(c, decodedC)
+    }
+    
+    func testECPointBytes() throws {
+        let str = "02d6b2141c21e4f337e9b065a031a6269fb5a49253094fc6243d38662eb765db00"
+        XCTAssertNoThrow(try Constant(withECPointBytes: stringToBytes(str)!))
+    }
+    
+    func testErgoBoxRoundtrip() throws {
+        let json = """
+            {
+              "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+              "value": 67500000000,
+              "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+              "assets": [],
+              "creationHeight": 284761,
+              "additionalRegisters": {},
+              "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+              "index": 1
+            }
+        """
+        let ergoBox = try ErgoBox(withJson: json)
+        let c = Constant(withErgoBox: ergoBox)
+        let encoded = try c.toBase16String()
+        let decodedC = try Constant(withBase16Str: encoded)
+        XCTAssertEqual(c, decodedC)
+    }
+}
+
+// The following two functions copied from: https://stackoverflow.com/a/49890939
+
+func toPairsOfChars(pairs: [String], string: String) -> [String] {
+    if string.count == 0 {
+        return pairs
+    }
+    var pairsMod = pairs
+    pairsMod.append(String(string.prefix(2)))
+    return toPairsOfChars(pairs: pairsMod, string: String(string.dropFirst(2)))
+}
+
+func stringToBytes(_ string: String) -> [UInt8]? {
+    // omit error checking: remove '0x', make sure even, valid chars
+    let pairs = toPairsOfChars(pairs: [], string: string)
+    return pairs.map { UInt8($0, radix: 16)! }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoBoxTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoBoxTests.swift
new file mode 100644
index 000000000..defc6d6d3
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoBoxTests.swift
@@ -0,0 +1,69 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class ErgoBoxTests: XCTestCase {
+    func testBoxId() throws {
+        let str = "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e"
+        let boxId = try BoxId(withString: str)
+        let newStr = boxId.toString()
+        XCTAssertEqual(str, newStr)
+    }
+    
+    func testBoxValue() throws {
+        let amount = Int64(12345678)
+        let boxValue = try BoxValue(fromInt64: amount)
+        XCTAssertEqual(boxValue.toInt64(), amount)
+    }
+    
+    static func generateErgoBoxComponents() throws -> (BoxId, BoxValue, ErgoTree, Contract) {
+        
+        let boxId = try BoxId(withString: "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e")
+        let boxValue = try BoxValue(fromInt64: Int64(67500000000))
+        let ergoTree = try ErgoTree(fromBase16EncodedString: "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301")
+        let contract = Contract(fromErgoTree: ergoTree)
+        return (boxId, boxValue, ergoTree, contract)
+    }
+    
+    func testErgoBoxInitializer() throws {
+        let (boxId, boxValue, ergoTree, contract) = try ErgoBoxTests.generateErgoBoxComponents()
+        let txId = try TxId(withString: "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9")
+        let tokens = Tokens()
+        let creationHeight = UInt32(284761)
+        let ergoBox = try
+            ErgoBox(
+                boxValue: boxValue,
+                creationHeight: creationHeight,
+                contract: contract,
+                txId: txId,
+                index: UInt16(1),
+                tokens: tokens
+            )
+        XCTAssertEqual(ergoBox.getCreationHeight(), creationHeight)
+        XCTAssertEqual(ergoBox.getBoxId(), boxId )
+        XCTAssertEqual(ergoBox.getBoxValue(), boxValue )
+        XCTAssertEqual(ergoBox.getErgoTree(), ergoTree)
+    }
+    
+    func testErgoBoxJSON() throws {
+        let json = """
+            {
+              "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+              "value": 67500000000,
+              "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+              "assets": [],
+              "creationHeight": 284761,
+              "additionalRegisters": {},
+              "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+              "index": 1
+            }
+        """
+        let ergoBox = try ErgoBox(withJson: json)
+        let (boxId, boxValue, ergoTree, _) = try ErgoBoxTests.generateErgoBoxComponents()
+        XCTAssertEqual(ergoBox.getCreationHeight(), UInt32(284761))
+        XCTAssertEqual(ergoBox.getBoxId(), boxId )
+        XCTAssertEqual(ergoBox.getBoxValue(), boxValue )
+        XCTAssertEqual(ergoBox.getErgoTree(), ergoTree)
+    }
+}
+
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoStateContextTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoStateContextTests.swift
new file mode 100644
index 000000000..59620841b
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoStateContextTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class ErgoStateContextTests: XCTestCase {
+    func testErgoStateContext() throws {
+        let headerJSON = HeaderTests.jsonHeaderExample()
+        let blockHeader = try BlockHeader(withJson: headerJSON)
+        let preHeader = PreHeader(withBlockHeader: blockHeader)
+        var blockHeadersJSON = Array(repeating: HeaderTests.jsonHeaderExample(), count: 10)
+        var blockHeaders = try BlockHeaders(fromJSON: blockHeadersJSON)
+        XCTAssertNoThrow(try ErgoStateContext(preHeader: preHeader, headers: blockHeaders))
+        
+        // Now test for incorrect number of block headers
+        blockHeadersJSON = Array(repeating: HeaderTests.jsonHeaderExample(), count: 8)
+        blockHeaders = try BlockHeaders(fromJSON: blockHeadersJSON)
+        XCTAssertThrowsError(try ErgoStateContext(preHeader: preHeader, headers: blockHeaders))
+    }
+    
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoTreeTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoTreeTests.swift
new file mode 100644
index 000000000..8cc0c9513
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/ErgoTreeTests.swift
@@ -0,0 +1,60 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class ErgoTreeTests: XCTestCase {
+    
+    static func ergoTreeBase16Example() -> String {
+        return "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301"
+    }
+    
+    static func ergoTreeExample() throws -> ErgoTree {
+        return
+          try ErgoTree(fromBase16EncodedString: ErgoTreeTests.ergoTreeBase16Example())
+    }
+    
+    func testEncoding() throws {
+        let tree = try ErgoTreeTests.ergoTreeExample()
+        XCTAssertEqual(try tree.toBase16EncodedString(), ErgoTreeTests.ergoTreeBase16Example())
+        XCTAssertNoThrow(try tree.toBytes())
+        XCTAssertNoThrow(try tree.toTemplateBytes())
+    }
+    func testConstantsLen() throws {
+        let tree = try ErgoTreeTests.ergoTreeExample()
+        XCTAssertEqual(UInt(2), try tree.constantsLength())
+    }
+    
+    func testGetConstant() throws {
+        let tree = try ErgoTreeTests.ergoTreeExample()
+        XCTAssertNotNil(try tree.getConstant(index: UInt(0)))
+        XCTAssertNotNil(try tree.getConstant(index: UInt(1)))
+        XCTAssertNil(try tree.getConstant(index: UInt(2)))
+    }
+    
+    
+    func testWithConstant() throws {
+        let treeBytesStr = "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301"
+        let tree = try ErgoTree(fromBase16EncodedString: treeBytesStr)
+        XCTAssertEqual(try tree.constantsLength(), UInt(2))
+        let constant = Constant(withInt32: Int32(99))
+        try tree.withConstant(index: UInt(0), constant: constant)
+        XCTAssertEqual(try tree.getConstant(index: UInt(0))!.toInt32(), Int32(99))
+    }
+    
+    func testWithConstantOutOfBounds() throws {
+        let treeBytesStr = "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301"
+        let tree = try ErgoTree(fromBase16EncodedString: treeBytesStr)
+        XCTAssertEqual(try tree.constantsLength(), UInt(2))
+        let constant = Constant(withInt32: Int32(99))
+        XCTAssertThrowsError(try tree.withConstant(index: UInt(3), constant: constant))
+    }
+    
+    func testWithConstantTypeMismatch() throws {
+        let treeBytesStr = "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301"
+        let tree = try ErgoTree(fromBase16EncodedString: treeBytesStr)
+        XCTAssertEqual(try tree.constantsLength(), UInt(2))
+        let constant = Constant(withInt64: Int64(324234))
+        XCTAssertThrowsError(try tree.withConstant(index: UInt(0), constant: constant))
+    }
+}
+
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/HeaderTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/HeaderTests.swift
new file mode 100644
index 000000000..c112e36ff
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/HeaderTests.swift
@@ -0,0 +1,74 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class HeaderTests: XCTestCase {
+    static func jsonHeaderExample() -> String {
+        return """
+        {
+        "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"
+        }
+        """
+    }
+    
+    func testBlockHeaders() throws {
+        let json = HeaderTests.jsonHeaderExample()
+        let blockHeader = try BlockHeader(withJson: json)
+        XCTAssertNoThrow(PreHeader(withBlockHeader: blockHeader))
+        XCTAssertThrowsError(try BlockHeader(withJson: ""))
+        let headers = BlockHeaders()
+        for _ in 1...10 {
+            XCTAssertNoThrow(headers.add(blockHeader: blockHeader ))
+        }
+        XCTAssertEqual(headers.len(), 10)
+        XCTAssertNotNil(headers.get(index: 9))
+        XCTAssertNil(headers.get(index: 10))
+    }
+    
+    static func generateBlockHeadersFromJSON() throws -> BlockHeaders {
+        let count = 10
+        let json1 = Array(
+            repeating: HeaderTests.jsonHeaderExample().data(using: .utf8, allowLossyConversion: false)!,
+            count: count
+        )
+        return try BlockHeaders(fromJSON: json1)
+    }
+    
+    func testBlockHeadersFromJSON() throws {
+        let count = 20
+        
+        // First try with array of strings
+        let json = Array(repeating: HeaderTests.jsonHeaderExample(), count: count)
+        var headers = try BlockHeaders(fromJSON: json)
+        XCTAssertEqual(headers.len(), UInt(count))
+        
+        // Now try [Data] form
+        let json1 = Array(
+            repeating: HeaderTests.jsonHeaderExample().data(using: .utf8, allowLossyConversion: false)!,
+            count: count
+        )
+        headers = try BlockHeaders(fromJSON: json1)
+        XCTAssertEqual(headers.len(), UInt(count))
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/SecretKeyTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/SecretKeyTests.swift
new file mode 100644
index 000000000..57f1ac8d1
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/SecretKeyTests.swift
@@ -0,0 +1,13 @@
+
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class SecreteKeyTests: XCTestCase {
+    func testSecretKey() throws {
+        let key = SecretKey()
+        let bytes = key.toBytes()
+        let newKey = try SecretKey(fromBytes: bytes)
+        XCTAssertEqual(bytes, newKey.toBytes())
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift
new file mode 100644
index 000000000..44f52d440
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift
@@ -0,0 +1,57 @@
+
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class TokenIdTests: XCTestCase {
+    func testTokenIdFromBoxId() throws {
+        let str = "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e"
+        let boxId = try BoxId(withString: str)
+        let tokenId = TokenId(fromBoxId: boxId)
+        XCTAssertNoThrow(tokenId.toBase16EncodedString())
+    }
+    
+    func testTokenIdFromStr() throws {
+        let str = "19475d9a78377ff0f36e9826cec439727bea522f6ffa3bda32e20d2f8b3103ac"
+        let tokenId = try TokenId(fromBase16EncodedString: str )
+        XCTAssertEqual(tokenId.toBase16EncodedString(), str)
+    }
+    
+    func testTokenAmount() throws {
+        let amount = Int64(12345678)
+        let tokenAmount = try TokenAmount(fromInt64: amount)
+        XCTAssertEqual(tokenAmount.toInt64(), amount)
+    }
+    
+    func testToken() throws {
+        let str = "19475d9a78377ff0f36e9826cec439727bea522f6ffa3bda32e20d2f8b3103ac"
+        let tokenId = try TokenId(fromBase16EncodedString: str )
+        let amount = Int64(12345678)
+        let tokenAmount = try TokenAmount(fromInt64: amount)
+        let token = Token(tokenId: tokenId, tokenAmount: tokenAmount)
+        let newTokenId = token.getId()
+        let newTokenAmount = token.getAmount()
+        XCTAssertEqual(newTokenId.toBase16EncodedString(), str)
+        XCTAssertEqual(tokenAmount, newTokenAmount)
+    }
+    
+    func testTokens() throws {
+        let tokens = Tokens()
+        XCTAssertEqual(tokens.len(), UInt(0))
+        XCTAssertNil(tokens.get(index: 3))
+        let str = "19475d9a78377ff0f36e9826cec439727bea522f6ffa3bda32e20d2f8b3103ac"
+        let tokenId = try TokenId(fromBase16EncodedString: str )
+        let amount = Int64(12345678)
+        let tokenAmount = try TokenAmount(fromInt64: amount)
+        let token = Token(tokenId: tokenId, tokenAmount: tokenAmount)
+        
+        for _ in 1...255 {
+            try tokens.add(token: token)
+        }
+        XCTAssertEqual(tokens.len(), UInt(255))
+        XCTAssertNotNil(tokens.get(index: 254))
+        
+        // Add 256th token, expecting error
+        XCTAssertThrowsError(try tokens.add(token: token))
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
new file mode 100644
index 000000000..d0fe8b096
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TransactionTests.swift
@@ -0,0 +1,311 @@
+
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class TransactionTests: XCTestCase {
+    func testTxId() throws {
+        let str = "93d344aa527e18e5a221db060ea1a868f46b61e4537e6e5f69ecc40334c15e38"
+        let txId = try TxId(withString: str)
+        XCTAssertEqual(try txId.toString(), str)
+    }
+    
+    func testTxBuilder() throws {
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let jsonStr = """
+        {
+          "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+          "value": 67500000000,
+          "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+          "assets": [],
+          "creationHeight": 284761,
+          "additionalRegisters": {},
+          "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+          "index": 1
+        }
+        """
+        let json = [jsonStr.data(using: .utf8, allowLossyConversion: false)!]
+        let unspentBoxes = try ErgoBoxes(fromJSON: json)
+        let contract = try Contract(payToAddress: recipient)
+        let outboxValue = BoxValue.SAFE_USER_MIN()
+        let outbox = try ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0)).build()
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        let dataInputs = DataInputs()
+        let boxSelector = SimpleBoxSelector()
+        let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
+        let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
+        let txBuilder = TxBuilder(
+            boxSelection: boxSelection,
+            outputCandidates: txOutputs,
+            currentHeight: 0,
+            feeAmount: fee,
+            changeAddress: changeAddress,
+            minChangeValue: minChangeValue
+        )
+        txBuilder.setDataInputs(dataInputs: dataInputs)
+        let tx = try txBuilder.build()
+        XCTAssertNoThrow(try tx.toJsonEIP12())
+    }
+    
+    func testSignTx() throws {
+        let sk = SecretKey()
+        let inputContract = try Contract(payToAddress: sk.getAddress())
+        let str = "93d344aa527e18e5a221db060ea1a868f46b61e4537e6e5f69ecc40334c15e38"
+        let txId = try TxId(withString: str)
+        let inputBox = try ErgoBox(boxValue: BoxValue(fromInt64: Int64(1000000000)), creationHeight: 0, contract: inputContract, txId: txId, index: 0, tokens: Tokens())
+        
+        // Create transaction that spends the 'simulated' box
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let unspentBoxes = ErgoBoxes()
+        unspentBoxes.add(ergoBox: inputBox)
+        let contract = try Contract(payToAddress: recipient)
+        let outboxValue = BoxValue.SAFE_USER_MIN()
+        let outbox = try ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0)).build()
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        let dataInputs = DataInputs()
+        let boxSelector = SimpleBoxSelector()
+        // Note that swift forbids integer overflow/underflow by default with hard crash
+        let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
+        let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
+        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: 0, feeAmount: fee, changeAddress: changeAddress, minChangeValue: minChangeValue)
+        txBuilder.setDataInputs(dataInputs: dataInputs)
+        let tx = try txBuilder.build()
+        XCTAssertNoThrow(try tx.toJsonEIP12())
+        
+        let txDataInputs = try ErgoBoxes(fromJSON: [])
+        let blockHeaders = try HeaderTests.generateBlockHeadersFromJSON()
+        let preHeader = PreHeader(withBlockHeader: blockHeaders.get(index: UInt(0))!)
+        let ctx = try ErgoStateContext(preHeader: preHeader, headers: blockHeaders)
+        let secretKeys = SecretKeys()
+        secretKeys.add(secretKey: sk)
+        let wallet = Wallet(secrets: secretKeys)
+        let signedTx = try wallet.signTransaction(stateContext: ctx, unsignedTx: tx, boxesToSpend: unspentBoxes, dataBoxes: txDataInputs)
+        XCTAssertNoThrow(try signedTx.toJsonEIP12())
+    }
+    
+    func testMintToken() throws {
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let jsonStr = """
+        {
+          "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+          "value": 67500000000,
+          "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+          "assets": [],
+          "creationHeight": 284761,
+          "additionalRegisters": {},
+          "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+          "index": 1
+        }
+        """
+        let json = [jsonStr.data(using: .utf8, allowLossyConversion: false)!]
+        let unspentBoxes = try ErgoBoxes(fromJSON: json)
+        let contract = try Contract(payToAddress: recipient)
+        let outboxValue = BoxValue.SAFE_USER_MIN()
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        
+        let boxSelector = SimpleBoxSelector()
+        // Note that swift forbids integer overflow/underflow by default with hard crash
+        let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
+        let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
+        
+        // Mint token
+        let tokenId = TokenId(fromBoxId: boxSelection.getBoxes().get(index: UInt(0))!.getBoxId())
+        let token = Token(tokenId: tokenId, tokenAmount: try TokenAmount(fromInt64: Int64(1)))
+        let boxBuilder = ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0))
+            .mintToken(token: token, tokenName: "TKN", tokenDesc: "token desc", numDecimals: UInt(2))
+        let outbox = try boxBuilder.build()
+        
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        let dataInputs = DataInputs()
+        let txBuilder = TxBuilder(
+            boxSelection: boxSelection,
+            outputCandidates: txOutputs,
+            currentHeight: 0,
+            feeAmount: fee,
+            changeAddress: changeAddress,
+            minChangeValue: minChangeValue
+        )
+        txBuilder.setDataInputs(dataInputs: dataInputs)
+        let tx = try txBuilder.build()
+        XCTAssertNoThrow(try tx.toJsonEIP12())
+    }
+    
+    func testBurnToken() throws {
+        let jsonStr =
+            """
+                {
+                  "boxId": "0cf7b9e71961cc473242de389c8e594a4e5d630ddd2e4e590083fb0afb386341",
+                  "value": 11491500000,
+                  "ergoTree": "100f040005c801056404000e2019719268d230fd9093e4db0e2e42a07883ffe976e77c7419efc1bb218a05d4ba04000500043c040204c096b10204020101040205c096b1020400d805d601b2a5730000d602e4c6a70405d6039c9d720273017302d604b5db6501fed9010463ededed93e4c67204050ec5a7938cb2db6308720473030001730492e4c672040605997202720390e4c6720406059a72027203d605b17204ea02d1edededededed93cbc27201e4c6a7060e917205730593db63087201db6308a793e4c6720104059db072047306d9010641639a8c720601e4c68c72060206057e72050593e4c6720105049ae4c6a70504730792c1720199c1a77e9c9a720573087309058cb072048602730a730bd901063c400163d802d6088c720601d6098c72080186029a7209730ceded8c72080293c2b2a5720900d0cde4c68c720602040792c1b2a5720900730d02b2ad7204d9010663cde4c672060407730e00",
+                  "assets": [
+                    {
+                      "tokenId": "19475d9a78377ff0f36e9826cec439727bea522f6ffa3bda32e20d2f8b3103ac",
+                      "amount": 1
+                    }
+                  ],
+                  "creationHeight": 348198,
+                  "additionalRegisters": {
+                    "R4": "059acd9109",
+                    "R5": "04f2c02a",
+                    "R6": "0e20277c78751ff6f68d4dcd082eeea9506324911a875b6b9cd4d177d4fcab061327"
+                  },
+                  "transactionId": "5ed0e572a8c097b053965519a696f413f7be02754345e8ed650377e29a6dedb3",
+                  "index": 0
+                }
+            """
+        let json = [jsonStr.data(using: .utf8, allowLossyConversion: false)!]
+        let unspentBoxes = try ErgoBoxes(fromJSON: json)
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let tokenId = try TokenId(fromBase16EncodedString: "19475d9a78377ff0f36e9826cec439727bea522f6ffa3bda32e20d2f8b3103ac")
+        let token = Token(tokenId: tokenId, tokenAmount: try TokenAmount(fromInt64: Int64(1)))
+        let boxSelector = SimpleBoxSelector()
+        let tokens = Tokens()
+        try tokens.add(token: token)
+        let outboxValue = BoxValue.SAFE_USER_MIN()
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        
+        // Note that swift forbids integer overflow/underflow by default with hard crash
+        let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
+        
+        // Select tokens from inputs
+        let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: tokens)
+        let contract = try Contract(payToAddress: recipient)
+        // but don't put selected tokens in the output box (burn them)
+        let boxBuilder = ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0))
+        let outbox = try boxBuilder.build()
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        let dataInputs = DataInputs()
+        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: UInt32(0), feeAmount: fee, changeAddress: changeAddress, minChangeValue: minChangeValue)
+        txBuilder.setDataInputs(dataInputs: dataInputs)
+        XCTAssertNoThrow(try txBuilder.build())
+    }
+    
+    func testUsingSignedTxAsInputInNewTx() throws {
+        let sk = SecretKey()
+        let inputContract = try Contract(payToAddress: sk.getAddress())
+        let str = "0000000000000000000000000000000000000000000000000000000000000000"
+        let txId = try TxId(withString: str)
+        let inputBox = try ErgoBox(boxValue: BoxValue(fromInt64: Int64(100000000000)), creationHeight: 0, contract: inputContract, txId: txId, index: 0, tokens: Tokens())
+        
+        // Create transaction that spends the 'simulated' box
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let unspentBoxes = ErgoBoxes()
+        unspentBoxes.add(ergoBox: inputBox)
+        let contract = try Contract(payToAddress: recipient)
+        let outboxValue = try BoxValue(fromInt64: Int64(10000000000))
+        let outbox = try ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0)).build()
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        let dataInputs = DataInputs()
+        let boxSelector = SimpleBoxSelector()
+        let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
+        let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
+        let txBuilder = TxBuilder(boxSelection: boxSelection, outputCandidates: txOutputs, currentHeight: 0, feeAmount: fee, changeAddress: changeAddress, minChangeValue: minChangeValue)
+        txBuilder.setDataInputs(dataInputs: dataInputs)
+        let tx = try txBuilder.build()
+        XCTAssertNoThrow(try tx.toJsonEIP12())
+        
+        let txDataInputs = try ErgoBoxes(fromJSON: [])
+        let blockHeaders = try HeaderTests.generateBlockHeadersFromJSON()
+        let preHeader = PreHeader(withBlockHeader: blockHeaders.get(index: UInt(0))!)
+        let ctx = try ErgoStateContext(preHeader: preHeader, headers: blockHeaders)
+        let secretKeys = SecretKeys()
+        secretKeys.add(secretKey: sk)
+        let wallet = Wallet(secrets: secretKeys)
+        let signedTx = try wallet.signTransaction(stateContext: ctx, unsignedTx: tx, boxesToSpend: unspentBoxes, dataBoxes: txDataInputs)
+        XCTAssertEqual(signedTx.getOutputs().get(index: UInt(0))!.getBoxValue().toInt64(), Int64(10000000000))
+        XCTAssertNoThrow(try signedTx.toJsonEIP12())
+        
+        // New tx
+        let newOutboxValue = try BoxValue(fromInt64: Int64(1000000000))
+        let newOutbox = try ErgoBoxCandidateBuilder(boxValue: newOutboxValue, contract: contract, creationHeight: UInt32(0)).build()
+        let newTxOutputs = ErgoBoxCandidates()
+        newTxOutputs.add(ergoBoxCandidate: newOutbox)
+        let newBoxSelector = SimpleBoxSelector()
+        let newTargetBalance = try BoxValue.sumOf(boxValue0: newOutboxValue, boxValue1: fee)
+        let newBoxSelection = try newBoxSelector.select(
+            inputs: signedTx.getOutputs(),
+            targetBalance: newTargetBalance,
+            targetTokens: Tokens()
+        )
+        let newTxBuilder = TxBuilder(
+            boxSelection: newBoxSelection,
+            outputCandidates: newTxOutputs,
+            currentHeight: UInt32(0),
+            feeAmount: fee,
+            changeAddress: changeAddress,
+            minChangeValue: minChangeValue
+        )
+        XCTAssertNoThrow(try newTxBuilder.build())
+    }
+    
+    func testTxFromUnsignedTx() throws {
+        let recipient = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let jsonStr = """
+        {
+          "boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
+          "value": 67500000000,
+          "ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
+          "assets": [],
+          "creationHeight": 284761,
+          "additionalRegisters": {},
+          "transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
+          "index": 1
+        }
+        """
+        let json = [jsonStr.data(using: .utf8, allowLossyConversion: false)!]
+        let unspentBoxes = try ErgoBoxes(fromJSON: json)
+        let contract = try Contract(payToAddress: recipient)
+        let outboxValue = BoxValue.SAFE_USER_MIN()
+        let outbox = try ErgoBoxCandidateBuilder(boxValue: outboxValue, contract: contract, creationHeight: UInt32(0)).build()
+        let txOutputs = ErgoBoxCandidates()
+        txOutputs.add(ergoBoxCandidate: outbox)
+        let fee = TxBuilder.SUGGESTED_TX_FEE()
+        let changeAddress = try Address(withTestnetAddress: "3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN")
+        let minChangeValue = BoxValue.SAFE_USER_MIN()
+        let dataInputs = DataInputs()
+        let boxSelector = SimpleBoxSelector()
+        // Note that swift forbids integer overflow/underflow by default with hard crash
+        let targetBalance = try BoxValue.sumOf(boxValue0: outboxValue, boxValue1: fee)
+        let boxSelection = try boxSelector.select(inputs: unspentBoxes, targetBalance: targetBalance, targetTokens: Tokens())
+        let txBuilder = TxBuilder(
+            boxSelection: boxSelection,
+            outputCandidates: txOutputs,
+            currentHeight: 0,
+            feeAmount: fee,
+            changeAddress: changeAddress,
+            minChangeValue: minChangeValue
+        )
+        txBuilder.setDataInputs(dataInputs: dataInputs)
+        let tx = try txBuilder.build()
+        let bytes = [1, 1, 2, 255].map{ UInt8($0) }
+        let proof = try ByteArray(fromBytes: bytes)
+        let proofs = ByteArrays()
+        proofs.add(byteArray: proof)
+        let signedTx = try Transaction.init(unsignedTx: tx, proofs: proofs)
+        XCTAssertEqual(signedTx.getInputs().get(index: UInt(0))!.getSpendingProof().toBytes(), bytes)
+    }
+    
+    func testWalletMnemonic() throws {
+        let phrase = "change me do not use me change me do not use me"
+        let pass = "password1234"
+        XCTAssertNoThrow(try Wallet(mnemonicPhrase: phrase, mnemonicPass: pass))
+    }
+}
diff --git a/bindings/ergo-lib-ios/xcode_ios_settings.png b/bindings/ergo-lib-ios/xcode_ios_settings.png
new file mode 100644
index 000000000..28a01d24d
Binary files /dev/null and b/bindings/ergo-lib-ios/xcode_ios_settings.png differ
diff --git a/bindings/ergo-lib-ios/xcode_linker_settings.png b/bindings/ergo-lib-ios/xcode_linker_settings.png
new file mode 100644
index 000000000..ac23de857
Binary files /dev/null and b/bindings/ergo-lib-ios/xcode_linker_settings.png differ
diff --git a/bindings/ergo-lib-jni/src/main/rust/lib.rs b/bindings/ergo-lib-jni/src/main/rust/lib.rs
index 865398cac..5fe8c1e33 100644
--- a/bindings/ergo-lib-jni/src/main/rust/lib.rs
+++ b/bindings/ergo-lib-jni/src/main/rust/lib.rs
@@ -15,7 +15,7 @@ extern crate log;
 
 mod exception;
 
-use ergo_lib_c_core::{address_delete, address_from_testnet, AddressPtr};
+use ergo_lib_c_core::address::{address_delete, address_from_testnet, AddressPtr};
 use exception::unwrap_exc_or;
 use jni::{
     objects::{JClass, JString},
diff --git a/bindings/ergo-lib-wasm/src/context_extension.rs b/bindings/ergo-lib-wasm/src/context_extension.rs
index 6ecd3f234..a72e0cdf7 100644
--- a/bindings/ergo-lib-wasm/src/context_extension.rs
+++ b/bindings/ergo-lib-wasm/src/context_extension.rs
@@ -7,7 +7,7 @@ use wasm_bindgen::prelude::*;
 extern crate derive_more;
 use derive_more::{From, Into};
 
-/// Proof of correctness of tx spending
+/// User-defined variables to be put into context
 #[wasm_bindgen]
 #[derive(PartialEq, Debug, Clone, From, Into)]
 pub struct ContextExtension(
diff --git a/bindings/ergo-lib-wasm/src/ergo_box.rs b/bindings/ergo-lib-wasm/src/ergo_box.rs
index e571de907..d071d5ecf 100644
--- a/bindings/ergo-lib-wasm/src/ergo_box.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_box.rs
@@ -283,7 +283,7 @@ pub struct ErgoBoxAssetsData(ergo_lib::wallet::box_selector::ErgoBoxAssetsData);
 
 #[wasm_bindgen]
 impl ErgoBoxAssetsData {
-    /// Create empty SimpleBoxSelector
+    /// Create new instance
     #[wasm_bindgen(constructor)]
     pub fn new(value: &BoxValue, tokens: &Tokens) -> Self {
         ErgoBoxAssetsData(ergo_lib::wallet::box_selector::ErgoBoxAssetsData {
diff --git a/bindings/ergo-lib-wasm/src/token.rs b/bindings/ergo-lib-wasm/src/token.rs
index 1d9c13bfa..994ad13c4 100644
--- a/bindings/ergo-lib-wasm/src/token.rs
+++ b/bindings/ergo-lib-wasm/src/token.rs
@@ -23,7 +23,7 @@ pub struct TokenId(chain::token::TokenId);
 
 #[wasm_bindgen]
 impl TokenId {
-    /// Create token id from erbo box id (32 byte digest)
+    /// Create token id from ergo box id (32 byte digest)
     pub fn from_box_id(box_id: &BoxId) -> TokenId {
         let box_id: chain::ergo_box::BoxId = box_id.clone().into();
         TokenId(chain::token::TokenId::from(box_id))
diff --git a/bindings/ergo-lib-wasm/src/tx_builder.rs b/bindings/ergo-lib-wasm/src/tx_builder.rs
index 34057f16a..e5ec511a2 100644
--- a/bindings/ergo-lib-wasm/src/tx_builder.rs
+++ b/bindings/ergo-lib-wasm/src/tx_builder.rs
@@ -64,7 +64,7 @@ impl TxBuilder {
             .map(UnsignedTransaction::from)
     }
 
-    /// Get inputs
+    /// Get box selection
     pub fn box_selection(&self) -> BoxSelection {
         self.0.box_selection().into()
     }
@@ -89,7 +89,7 @@ impl TxBuilder {
         self.0.fee_amount().into()
     }
 
-    /// Get change
+    /// Get change address
     pub fn change_address(&self) -> Address {
         self.0.change_address().into()
     }


Test patch: