From 263362ad7e90f6f1329cf3a6a1124e5b9778e586 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Al=D0=B5x=D0=B0nder=20Lysik=D0=BEv?=
 <alexander.lysikov@demlabs.net>
Date: Tue, 18 Aug 2020 22:32:38 +0500
Subject: [PATCH] added command 'ledger' and 'token'

---
 modules/chain/dap_chain.c                     |  31 +-
 modules/chain/include/dap_chain.h             |   3 +
 modules/global-db/dap_chain_global_db_hist.c  |   6 +-
 .../global-db/include/dap_chain_global_db.h   |   2 +-
 modules/net/dap_chain_node_cli.c              |  13 +
 modules/net/dap_chain_node_cli_cmd_tx.c       | 816 +++++++++++++++++-
 .../net/include/dap_chain_node_cli_cmd_tx.h   |  13 +
 7 files changed, 871 insertions(+), 13 deletions(-)

diff --git a/modules/chain/dap_chain.c b/modules/chain/dap_chain.c
index dd0910e8f6..6f476a3056 100644
--- a/modules/chain/dap_chain.c
+++ b/modules/chain/dap_chain.c
@@ -93,8 +93,35 @@ void dap_chain_deinit(void)
     dap_chain_item_t * l_item = NULL, *l_tmp = NULL;
     pthread_rwlock_wrlock(&s_chain_items_rwlock);
     HASH_ITER(hh, s_chain_items, l_item, l_tmp) {
-          dap_chain_delete(s_chain_items->chain);
-        }
+          dap_chain_delete(l_item->chain);
+    }
+    pthread_rwlock_unlock(&s_chain_items_rwlock);
+}
+
+
+/**
+ * @brief dap_chain_deinit
+ * note: require dap_chain_enum_unlock() after
+ */
+dap_chain_t* dap_chain_enum(void** a_item)
+{
+    // if a_item == 0x1 then first item
+    dap_chain_item_t *l_item_start = (*a_item == 0x1) ? s_chain_items : (dap_chain_item_t*) *a_item;
+    dap_chain_item_t *l_item = NULL;
+    dap_chain_item_t *l_item_tmp = NULL;
+    pthread_rwlock_wrlock(&s_chain_items_rwlock);
+    HASH_ITER(hh, l_item_start, l_item, l_item_tmp) {
+        *a_item = l_item_tmp;
+        return l_item->chain;
+    }
+    return NULL ;
+}
+
+/**
+ * @brief dap_chain_enum_unlock
+ */
+void dap_chain_enum_unlock(void)
+{
     pthread_rwlock_unlock(&s_chain_items_rwlock);
 }
 
diff --git a/modules/chain/include/dap_chain.h b/modules/chain/include/dap_chain.h
index 0888d5eaa2..ee2419d5e7 100644
--- a/modules/chain/include/dap_chain.h
+++ b/modules/chain/include/dap_chain.h
@@ -158,6 +158,9 @@ typedef struct dap_chain{
 int dap_chain_init(void);
 void dap_chain_deinit(void);
 
+dap_chain_t* dap_chain_enum(void** a_item);
+void dap_chain_enum_unlock(void);
+
 
 dap_chain_t * dap_chain_create(dap_ledger_t* a_ledger,const char * a_chain_net_name, const char * a_chain_name, dap_chain_net_id_t a_chain_net_id, dap_chain_id_t a_chain_id );
 
diff --git a/modules/global-db/dap_chain_global_db_hist.c b/modules/global-db/dap_chain_global_db_hist.c
index c2683b1560..e552cd7e71 100644
--- a/modules/global-db/dap_chain_global_db_hist.c
+++ b/modules/global-db/dap_chain_global_db_hist.c
@@ -12,7 +12,7 @@
 #include "dap_chain_global_db_hist.h"
 
 #include "uthash.h"
-// for dap_db_history_filter()
+// for dap_db_history()
 typedef struct dap_tx_data{
         dap_chain_hash_fast_t tx_hash;
         char tx_hash_str[70];
@@ -135,7 +135,7 @@ uint8_t* dap_db_log_pack(dap_global_db_obj_t *a_obj, size_t *a_data_size_out)
 }
 
 
-// for dap_db_history_filter()
+// for dap_db_history()
 static dap_store_obj_t* get_prev_tx(dap_global_db_obj_t *a_objs, dap_tx_data_t *a_tx_data)
 {
     if(!a_objs || !a_tx_data)
@@ -724,7 +724,7 @@ char* dap_db_history_addr(dap_chain_addr_t * a_addr, const char *a_group_mempool
  *
  * return history string
  */
-char* dap_db_history_filter(dap_chain_addr_t * a_addr, const char *a_group_mempool)
+char* dap_db_history(dap_chain_addr_t * a_addr, const char *a_group_mempool)
 {
     dap_string_t *l_str_out = dap_string_new(NULL);
     // load history
diff --git a/modules/global-db/include/dap_chain_global_db.h b/modules/global-db/include/dap_chain_global_db.h
index 4ac3c5a9a3..fdbc872c92 100644
--- a/modules/global-db/include/dap_chain_global_db.h
+++ b/modules/global-db/include/dap_chain_global_db.h
@@ -124,7 +124,7 @@ uint8_t* dap_db_log_pack(dap_global_db_obj_t *a_obj, size_t *a_data_size_out);
 // Get data according the history log
 //char* dap_db_history_tx(dap_chain_hash_fast_t * a_tx_hash, const char *a_group_mempool);
 //char* dap_db_history_addr(dap_chain_addr_t * a_addr, const char *a_group_mempool);
-//char* dap_db_history_filter(dap_chain_addr_t * a_addr, const char *a_group_mempool);
+//char* dap_db_history(dap_chain_addr_t * a_addr, const char *a_group_mempool);
 
 // Parse data from dap_db_log_pack()
 void* dap_db_log_unpack(const void *a_data, size_t a_data_size, size_t *a_store_obj_count);
diff --git a/modules/net/dap_chain_node_cli.c b/modules/net/dap_chain_node_cli.c
index 8ce67e110d..a14707aede 100644
--- a/modules/net/dap_chain_node_cli.c
+++ b/modules/net/dap_chain_node_cli.c
@@ -62,6 +62,7 @@
 #include "dap_list.h"
 #include "dap_chain_node_cli_cmd.h"
 #include "dap_chain_node_client.h"
+#include "dap_chain_node_cli_cmd_tx.h"
 #include "dap_chain_node_cli.h"
 
 //#include "dap_chain_node_cli.h"
@@ -957,6 +958,18 @@ int dap_chain_node_cli_init(dap_config_t * g_config)
     dap_chain_node_cli_cmd_item_create("tx_history", com_tx_history, NULL, "Transaction history (for address or by hash)",
             "tx_history  [-addr <addr> | -w <wallet name> | -tx <tx_hash>] -net <net name> -chain <chain name>\n");
 
+    // Ledger info
+    dap_chain_node_cli_cmd_item_create("ledger", com_ledger, NULL, "Ledger info",
+            "ledger list coins -net <network name>\n"
+            "ledger list coins_cond -net <network name>\n"
+            "ledger list addrs -net <network name>\n"
+            "ledger tx [all | -addr <addr> | -w <wallet name> | -tx <tx_hash>] [-chain <chain name>] -net <network name>\n");
+
+    // Token info
+    dap_chain_node_cli_cmd_item_create("token", com_token, NULL, "Token info",
+            "token list -net <network name>\n"
+            "token tx all name <token name> -net <network name> [-page_start <page>] [-page <page>]\n");
+
     // Log
     dap_chain_node_cli_cmd_item_create ("print_log", com_print_log, NULL, "Print log info",
                 "print_log [ts_after <timestamp >] [limit <line numbers>]\n" );
diff --git a/modules/net/dap_chain_node_cli_cmd_tx.c b/modules/net/dap_chain_node_cli_cmd_tx.c
index c34c4e7e81..d70e24efb8 100644
--- a/modules/net/dap_chain_node_cli_cmd_tx.c
+++ b/modules/net/dap_chain_node_cli_cmd_tx.c
@@ -26,15 +26,18 @@
 #include <stddef.h>
 #include <pthread.h>
 
-#include <dap_common.h>
-#include <dap_enc_base58.h>
-#include <dap_strfuncs.h>
-#include <dap_string.h>
-#include <dap_list.h>
-#include <dap_hash.h>
+#include "dap_common.h"
+#include "dap_enc_base58.h"
+#include "dap_strfuncs.h"
+#include "dap_string.h"
+#include "dap_list.h"
+#include "dap_hash.h"
 
+#include "dap_chain_wallet.h"
+#include "dap_chain_datum.h"
+#include "dap_chain_datum_token.h"
 #include "dap_chain_datum_tx_items.h"
-
+#include "dap_chain_node_cli.h"
 #include "dap_chain_node_cli_cmd_tx.h"
 
 #define LOG_TAG "chain_node_cli_cmd_tx"
@@ -676,3 +679,802 @@ char* dap_db_history_addr(dap_chain_addr_t * a_addr, dap_chain_t * a_chain, cons
     char *l_ret_str = l_str_out ? dap_string_free(l_str_out, false) : NULL;
     return l_ret_str;
 }
+
+static char* dap_db_history_token_list(dap_chain_t * a_chain, const char *a_token_name, const char *a_hash_out_type, size_t *a_token_num)
+{
+    dap_string_t *l_str_out = dap_string_new(NULL);
+    *a_token_num  = 0;
+    bool l_tx_hash_found = false;
+    // list all transactions
+    dap_tx_data_t *l_tx_data_hash = NULL;
+    // load transactions
+    dap_chain_atom_iter_t *l_atom_iter = a_chain->callback_atom_iter_create(a_chain);
+    dap_chain_atom_ptr_t *l_atom = a_chain->callback_atom_iter_get_first(l_atom_iter);
+    size_t l_atom_size = a_chain->callback_atom_get_size(l_atom);
+    while(l_atom && l_atom_size) {
+        dap_chain_datum_t *l_datum =
+                a_chain->callback_atom_get_datum ?
+                        a_chain->callback_atom_get_datum(l_atom) : (dap_chain_datum_t*) l_atom;
+        if(!l_datum) {
+            // go to next transaction
+            l_atom = a_chain->callback_atom_iter_get_next(l_atom_iter);
+            l_atom_size = a_chain->callback_atom_get_size(l_atom);
+            log_it(L_ERROR, "datum=NULL for atom=0x%x", l_atom);
+            continue;
+        }
+        char l_time_str[70];
+        // get time of create datum
+        if(dap_time_to_str_rfc822(l_time_str, 71, l_datum->header.ts_create) < 1)
+            l_time_str[0] = '\0';
+        if(l_datum->header.type_id==DAP_CHAIN_DATUM_TOKEN_DECL) {
+            dap_chain_datum_token_t *l_token = (dap_chain_datum_token_t*) l_datum->data;
+            if(!a_token_name || !dap_strcmp(l_token->ticker, a_token_name)) {
+                dap_string_append_printf(l_str_out, "token %s, created: %s\n", l_token->ticker, l_time_str);
+                switch (l_token->type) {
+                // Simple private token decl
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_SIMPLE:
+                    dap_string_append_printf(l_str_out, "  total_supply: %.0llf(%llu), signs: valid/total %02d/%02d \n",
+                            l_token->header_private.total_supply / DATOSHI_LD,
+                            l_token->header_private.total_supply,
+                            l_token->header_private.signs_valid, l_token->header_private.signs_total);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_PRIVATE_DECL:
+                    dap_string_append_printf(l_str_out, "  tsd_total_size: %llu, flags: 0x%x \n",
+                            l_token->header_private_decl.tsd_total_size,
+                            l_token->header_private_decl.flags);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_PRIVATE_UPDATE:
+                    dap_string_append_printf(l_str_out, "  tsd_total_size: %llu, padding: 0x%x \n",
+                            l_token->header_private_update.tsd_total_size,
+                            l_token->header_private_update.padding);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_PUBLIC: {
+                    char *l_addr = dap_chain_addr_to_str(&l_token->header_public.premine_address);
+                    dap_string_append_printf(l_str_out,
+                            " total_supply: %.0llf(%llu), flags: 0x%x\n, premine_supply: %llu, premine_address '%s'\n",
+                            l_token->header_public.total_supply / DATOSHI_LD,
+                            l_token->header_public.total_supply,
+                            l_token->header_public.flags,
+                            l_token->header_public.premine_supply,
+                            l_addr ? l_addr : "-");
+                    DAP_DELETE(l_addr);
+                }
+                    break;
+                default:
+                    dap_string_append_printf(l_str_out, "unknown token type: 0x%x\n", l_token->type);
+                    break;
+
+                }
+                dap_string_append_printf(l_str_out, "\n");
+                (*a_token_num)++;
+            }
+        }
+
+        // go to next transaction
+        l_atom = a_chain->callback_atom_iter_get_next(l_atom_iter);
+        l_atom_size = a_chain->callback_atom_get_size(l_atom);
+    }
+
+    a_chain->callback_atom_iter_delete(l_atom_iter);
+    char *l_ret_str = l_str_out ? dap_string_free(l_str_out, false) : NULL;
+    return l_ret_str;
+}
+
+/**
+ * Get data according the history log
+ *
+ * return history string
+ */
+static char* dap_db_history_filter(dap_chain_t * a_chain, const char *a_token_name, const char *a_hash_out_type)
+{
+    dap_string_t *l_str_out = dap_string_new(NULL);
+
+    bool l_tx_hash_found = false;
+    // list all transactions
+    dap_tx_data_t *l_tx_data_hash = NULL;
+    // load transactions
+    dap_chain_atom_iter_t *l_atom_iter = a_chain->callback_atom_iter_create(a_chain);
+    dap_chain_atom_ptr_t *l_atom = a_chain->callback_atom_iter_get_first(l_atom_iter);
+    size_t l_atom_size = a_chain->callback_atom_get_size(l_atom);
+    size_t l_datum_num = 0, l_token_num = 0, l_emission_num = 0, l_tx_num = 0;
+    while(l_atom && l_atom_size) {
+        dap_chain_datum_t *l_datum =
+                a_chain->callback_atom_get_datum ?
+                        a_chain->callback_atom_get_datum(l_atom) : (dap_chain_datum_t*) l_atom;
+        if(!l_datum) {
+            // go to next transaction
+            l_atom = a_chain->callback_atom_iter_get_next(l_atom_iter);
+            l_atom_size = a_chain->callback_atom_get_size(l_atom);
+            log_it(L_ERROR, "datum=NULL for atom=0x%x", l_atom);
+            continue;
+        }
+        char l_time_str[70];
+        // get time of create datum
+        if(dap_time_to_str_rfc822(l_time_str, 71, l_datum->header.ts_create) < 1)
+            l_time_str[0] = '\0';
+        switch (l_datum->header.type_id) {
+
+        // token
+        case DAP_CHAIN_DATUM_TOKEN_DECL: {
+            dap_chain_datum_token_t *l_token = (dap_chain_datum_token_t*) l_datum->data;
+            if(!a_token_name || !dap_strcmp(l_token->ticker, a_token_name)) {
+                dap_string_append_printf(l_str_out, "token %s, created: %s\n", l_token->ticker, l_time_str);
+                switch (l_token->type) {
+                // Simple private token decl
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_SIMPLE:
+                    dap_string_append_printf(l_str_out, "  total_supply: %.0llf(%llu), signs: valid/total %02d/%02d \n",
+                            l_token->header_private.total_supply / DATOSHI_LD,
+                            l_token->header_private.total_supply,
+                            l_token->header_private.signs_valid, l_token->header_private.signs_total);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_PRIVATE_DECL:
+                    dap_string_append_printf(l_str_out, "  tsd_total_size: %llu, flags: 0x%x \n",
+                            l_token->header_private_decl.tsd_total_size,
+                            l_token->header_private_decl.flags);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_PRIVATE_UPDATE:
+                    dap_string_append_printf(l_str_out, "  tsd_total_size: %llu, padding: 0x%x \n",
+                            l_token->header_private_update.tsd_total_size,
+                            l_token->header_private_update.padding);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_TYPE_PUBLIC: {
+                    char *l_addr = dap_chain_addr_to_str(&l_token->header_public.premine_address);
+                    dap_string_append_printf(l_str_out,
+                            " total_supply: %.0llf(%llu), flags: 0x%x\n, premine_supply: %llu, premine_address '%s'\n",
+                            l_token->header_public.total_supply / DATOSHI_LD,
+                            l_token->header_public.total_supply,
+                            l_token->header_public.flags,
+                            l_token->header_public.premine_supply,
+                            l_addr ? l_addr : "-");
+                    DAP_DELETE(l_addr);
+                }
+                    break;
+                default:
+                    dap_string_append_printf(l_str_out, "unknown token type: 0x%x\n", l_token->type);
+                    break;
+
+                }
+                dap_string_append_printf(l_str_out, "\n");
+                l_token_num++;
+            }
+        }
+            break;
+
+            // emission
+        case DAP_CHAIN_DATUM_TOKEN_EMISSION: {
+            dap_chain_datum_token_emission_t *l_token_em = (dap_chain_datum_token_emission_t*) l_datum->data;
+            if(!a_token_name || !dap_strcmp(l_token_em->hdr.ticker, a_token_name)) {
+                dap_string_append_printf(l_str_out, "emission: %.0llf(%llu) %s, type: %s, version: %d\n",
+                        l_token_em->hdr.value / DATOSHI_LD, l_token_em->hdr.value, l_token_em->hdr.ticker,
+                        c_dap_chain_datum_token_emission_type_str[l_token_em->hdr.type],
+                        l_token_em->hdr.version);
+                char * l_token_emission_address_str = dap_chain_addr_to_str(&(l_token_em->hdr.address));
+                dap_string_append_printf(l_str_out, "  to addr: %s\n", l_token_emission_address_str);
+                DAP_DELETE(l_token_emission_address_str);
+                switch (l_token_em->hdr.type) {
+                case DAP_CHAIN_DATUM_TOKEN_EMISSION_TYPE_UNDEFINED:
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_EMISSION_TYPE_AUTH:
+                    dap_string_append_printf(l_str_out, "  signs_count: %d\n", l_token_em->data.type_auth.signs_count);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_EMISSION_TYPE_ALGO:
+                    dap_string_append_printf(l_str_out, "  codename: %s\n", l_token_em->data.type_algo.codename);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_EMISSION_TYPE_ATOM_OWNER:
+                    dap_string_append_printf(l_str_out, " value_start: %.0llf(%llu), codename: %s\n",
+                            l_token_em->data.type_atom_owner.value_start / DATOSHI_LD,
+                            l_token_em->data.type_atom_owner.value_start,
+                            l_token_em->data.type_atom_owner.value_change_algo_codename);
+                    break;
+                case DAP_CHAIN_DATUM_TOKEN_EMISSION_TYPE_SMART_CONTRACT: {
+                    char *l_addr = dap_chain_addr_to_str(&l_token_em->data.type_presale.addr);
+                    // get time of create datum
+                    if(dap_time_to_str_rfc822(l_time_str, 71, l_token_em->data.type_presale.lock_time) < 1)
+                        l_time_str[0] = '\0';
+                    dap_string_append_printf(l_str_out, "  flags: 0x%x, lock_time: %s\n",
+                            l_token_em->data.type_presale.flags, l_time_str);
+                    dap_string_append_printf(l_str_out, "  addr: %s\n", l_addr);
+                    DAP_DELETE(l_addr);
+                }
+                    break;
+                }
+                dap_string_append_printf(l_str_out, "\n");
+                l_emission_num++;
+            }
+        }
+            break;
+
+            // transaction
+        case DAP_CHAIN_DATUM_TX:{
+            dap_chain_datum_tx_t *l_tx = (dap_chain_datum_tx_t*) l_datum->data;
+
+
+            // find Token items - present in emit transaction
+            dap_list_t *l_list_tx_token = dap_chain_datum_tx_items_get(l_tx, TX_ITEM_TYPE_TOKEN, NULL);
+            // find OUT items
+            dap_list_t *l_list_out_items = dap_chain_datum_tx_items_get(l_tx, TX_ITEM_TYPE_OUT, NULL);
+
+            dap_tx_data_t *l_tx_data = NULL;
+
+
+             // calc tx hash
+             dap_chain_hash_fast_t l_tx_hash;
+            dap_hash_fast(l_tx, dap_chain_datum_tx_get_size(l_tx), &l_tx_hash);
+            char *tx_hash_str;
+            char l_tx_hash_str[70];
+            dap_chain_hash_fast_to_str(&l_tx_hash, l_tx_hash_str, 70);
+            if(!dap_strcmp(a_hash_out_type, "hex"))
+                tx_hash_str = dap_strdup(l_tx_hash_str);
+            else
+                tx_hash_str = dap_enc_base58_from_hex_str_to_str(l_tx_hash_str);
+
+            dap_string_append_printf(l_str_out, "transaction: %s hash: %s\n", l_list_tx_token ? "(emit)" : "", tx_hash_str);
+            DAP_DELETE(tx_hash_str);
+
+            dap_list_t *l_list_tmp = l_list_out_items;
+            while(l_list_tmp) {
+                const dap_chain_tx_out_t *l_tx_out = (const dap_chain_tx_out_t*) l_list_tmp->data;
+                // save OUT item l_tx_out - only for first OUT item
+                if(!l_tx_data)
+                {
+                    // save tx hash
+                    l_tx_data = DAP_NEW_Z(dap_tx_data_t);
+                    dap_chain_hash_fast_t l_tx_hash;
+                    dap_hash_fast(l_tx, dap_chain_datum_tx_get_size(l_tx), &l_tx_hash);
+                    memcpy(&l_tx_data->tx_hash, &l_tx_hash, sizeof(dap_chain_hash_fast_t));
+                    memcpy(&l_tx_data->addr, &l_tx_out->addr, sizeof(dap_chain_addr_t));
+                    dap_chain_hash_fast_to_str(&l_tx_data->tx_hash, l_tx_data->tx_hash_str,
+                            sizeof(l_tx_data->tx_hash_str));
+                    l_tx_data->datum = DAP_NEW_SIZE(dap_chain_datum_t, l_atom_size);
+                    memcpy(l_tx_data->datum, l_datum, l_atom_size);
+                    // save token name
+                    if(l_list_tx_token) {
+                        dap_chain_tx_token_t *tk = l_list_tx_token->data;
+                        memcpy(l_tx_data->token_ticker, tk->header.ticker, sizeof(l_tx_data->token_ticker));
+                    }
+                    // take token from prev out item
+                    else {
+
+                        // find IN items
+                        dap_list_t *l_list_in_items = dap_chain_datum_tx_items_get(l_tx, TX_ITEM_TYPE_IN, NULL);
+                        dap_list_t *l_list_tmp_in = l_list_in_items;
+                        // find token_ticker in prev OUT items
+                        while(l_list_tmp_in) {
+                            const dap_chain_tx_in_t *l_tx_in =
+                                    (const dap_chain_tx_in_t*) l_list_tmp_in->data;
+                            dap_chain_hash_fast_t tx_prev_hash = l_tx_in->header.tx_prev_hash;
+
+                            //find prev OUT item
+                            dap_tx_data_t *l_tx_data_prev = NULL;
+                            HASH_FIND(hh, l_tx_data_hash, &tx_prev_hash, sizeof(dap_chain_hash_fast_t), l_tx_data_prev);
+                            if(l_tx_data_prev != NULL) {
+                                // fill token in l_tx_data from prev transaction
+                                if(l_tx_data) {
+                                    // get token from prev tx
+                                    memcpy(l_tx_data->token_ticker, l_tx_data_prev->token_ticker,
+                                            sizeof(l_tx_data->token_ticker));
+                                    break;
+                                }
+                            }
+                            l_list_tmp_in = dap_list_next(l_list_tmp_in);
+                        }
+                        if(l_list_in_items)
+                            dap_list_free(l_list_in_items);
+                    }
+                    HASH_ADD(hh, l_tx_data_hash, tx_hash, sizeof(dap_chain_hash_fast_t), l_tx_data);
+                }
+                l_list_tmp = dap_list_next(l_list_tmp);
+            }
+            if(l_list_out_items)
+                dap_list_free(l_list_out_items);
+
+            // found a_tx_hash now
+            // transaction time
+            if(l_tx->header.ts_created > 0) {
+                time_t rawtime = (time_t) l_tx->header.ts_created;
+                struct tm l_timeinfo = { 0 };
+                localtime_r(&rawtime, &l_timeinfo);
+                dap_string_append_printf(l_str_out, " %s", asctime(&l_timeinfo));
+            }
+
+            // find all OUT items in transaction
+            l_list_out_items = dap_chain_datum_tx_items_get(l_tx, TX_ITEM_TYPE_OUT, NULL);
+            l_list_tmp = l_list_out_items;
+            while(l_list_tmp) {
+                const dap_chain_tx_out_t *l_tx_out = (const dap_chain_tx_out_t*) l_list_tmp->data;
+                dap_tx_data_t *l_tx_data_prev = NULL;
+
+                const char *l_token_str = NULL;
+                if(l_tx_data)
+                    l_token_str = l_tx_data->token_ticker;
+                char *l_dst_to_str =
+                        (l_tx_out) ? dap_chain_addr_to_str(&l_tx_out->addr) :
+                        NULL;
+                dap_string_append_printf(l_str_out, " OUT item %lld %s to %s\n",
+                        l_tx_out->header.value,
+                        dap_strlen(l_token_str) > 0 ? l_token_str : "?",
+                        l_dst_to_str ? l_dst_to_str : "?"
+                                       );
+                DAP_DELETE(l_dst_to_str);
+                l_list_tmp = dap_list_next(l_list_tmp);
+            }
+
+            // find all IN items in transaction
+            dap_list_t *l_list_in_items = dap_chain_datum_tx_items_get(l_tx, TX_ITEM_TYPE_IN, NULL);
+            l_list_tmp = l_list_in_items;
+            // find cur addr in prev OUT items
+            while(l_list_tmp) {
+                const dap_chain_tx_in_t *l_tx_in = (const dap_chain_tx_in_t*) l_list_tmp->data;
+                dap_chain_hash_fast_t tx_prev_hash = l_tx_in->header.tx_prev_hash;
+                char l_tx_hash_str[70];
+                char *tx_hash_base58_str = NULL;
+                if(!dap_hash_fast_is_blank(&tx_prev_hash)) {
+                    tx_hash_base58_str = dap_enc_base58_from_hex_str_to_str(l_tx_data->tx_hash_str);
+                    dap_chain_hash_fast_to_str(&tx_prev_hash, l_tx_hash_str, sizeof(l_tx_hash_str));
+                }
+                else {
+                    strcpy(l_tx_hash_str, "Null");
+                    tx_hash_base58_str = dap_strdup("Null");
+                }
+                if(!dap_strcmp(a_hash_out_type, "hex"))
+                    dap_string_append_printf(l_str_out, " IN item \n  prev tx_hash %s\n", l_tx_hash_str);
+                else
+                    dap_string_append_printf(l_str_out, " IN item \n  prev tx_hash %s\n", tx_hash_base58_str);
+                DAP_DELETE(tx_hash_base58_str);
+
+                //find prev OUT item
+                dap_tx_data_t *l_tx_data_prev = NULL;
+                HASH_FIND(hh, l_tx_data_hash, &tx_prev_hash, sizeof(dap_chain_hash_fast_t), l_tx_data_prev);
+                if(l_tx_data_prev != NULL) {
+
+                    dap_chain_datum_t *l_datum_prev = get_prev_tx(l_tx_data_prev);
+                    dap_chain_datum_tx_t *l_tx_prev =
+                            l_datum_prev ? (dap_chain_datum_tx_t*) l_datum_prev->data : NULL;
+
+                    // find OUT items in prev datum
+                    dap_list_t *l_list_out_prev_items = dap_chain_datum_tx_items_get(l_tx_prev,
+                            TX_ITEM_TYPE_OUT, NULL);
+                    // find OUT item for IN item;
+                    dap_list_t *l_list_out_prev_item = dap_list_nth(l_list_out_prev_items,
+                            l_tx_in->header.tx_out_prev_idx);
+                    dap_chain_tx_out_t *l_tx_prev_out =
+                            l_list_out_prev_item ?
+                                                   (dap_chain_tx_out_t*) l_list_out_prev_item->data :
+                                                   NULL;
+                    // print value from prev out item
+                    dap_string_append_printf(l_str_out, "  prev OUT item value=%lld",
+                            l_tx_prev_out ? l_tx_prev_out->header.value : 0);
+                }
+                dap_string_append_printf(l_str_out, "\n");
+                l_list_tmp = dap_list_next(l_list_tmp);
+            }
+
+            if(l_list_tx_token)
+                dap_list_free(l_list_tx_token);
+            if(l_list_out_items)
+                dap_list_free(l_list_out_items);
+            if(l_list_in_items)
+                dap_list_free(l_list_in_items);
+            l_tx_hash_found = true;
+            l_tx_num++;
+        }
+            break;
+        default:
+            dap_string_append_printf(l_str_out, "unknown datum type=%d %lld %s to %s\n", l_datum->header.type_id);
+            break;
+        }
+        // go to next transaction
+        l_atom = a_chain->callback_atom_iter_get_next(l_atom_iter);
+        l_atom_size = a_chain->callback_atom_get_size(l_atom);
+        l_datum_num++;
+/*        continue;
+
+
+
+        //////// calc hash
+         dap_chain_hash_fast_t l_tx_hash;
+         dap_hash_fast(l_tx, dap_chain_datum_tx_get_size(l_tx), &l_tx_hash);
+         // search tx with a_tx_hash
+         if(!dap_hash_fast_compare(a_tx_hash, &l_tx_hash)) {
+         // go to next transaction
+         l_atom = a_chain->callback_atom_iter_get_next(l_atom_iter);
+         l_atom_size = a_chain->callback_atom_get_size(l_atom);
+         continue;
+         }
+
+
+
+
+
+
+
+        //break;
+
+        // go to next transaction
+        l_atom = a_chain->callback_atom_iter_get_next(l_atom_iter);
+        l_atom_size = a_chain->callback_atom_get_size(l_atom);
+        */
+
+    }
+    a_chain->callback_atom_iter_delete(l_atom_iter);
+    //total
+    dap_string_append_printf(l_str_out,
+            "---------------\ntokens: %u\nemissions: %u\ntransactions: %u\ntotal datums: %u", l_token_num,
+            l_emission_num, l_tx_num, l_datum_num);
+
+    // delete hashes
+    dap_tx_data_t *l_iter_current, *l_item_tmp;
+    HASH_ITER(hh, l_tx_data_hash , l_iter_current, l_item_tmp)
+    {
+        HASH_DEL(l_tx_data_hash, l_iter_current);
+        // delete datum
+        DAP_DELETE(l_iter_current->datum);
+        // delete struct
+        DAP_DELETE(l_iter_current);
+    }
+
+    // if no history
+    if(!l_str_out->len)
+        dap_string_append(l_str_out, "empty");
+    char *l_ret_str = l_str_out ? dap_string_free(l_str_out, false) : NULL;
+    return l_ret_str;
+}
+
+
+
+
+/**
+ * ledger command
+ *
+ */
+int com_ledger(int a_argc, char ** a_argv, void *a_arg_func, char **a_str_reply)
+{
+    enum { CMD_NONE, CMD_LIST, CMD_TX_HISTORY };
+    int arg_index = 1;
+    const char *l_addr_base58 = NULL;
+    const char *l_wallet_name = NULL;
+    const char *l_net_str = NULL;
+    const char *l_chain_str = NULL;
+    const char *l_tx_hash_str = NULL;
+
+    dap_chain_t * l_chain = NULL;
+    dap_chain_net_t * l_net = NULL;
+
+    const char * l_hash_out_type = NULL;
+    dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-H", &l_hash_out_type);
+    if(!l_hash_out_type)
+        l_hash_out_type = "base58";
+    if(dap_strcmp(l_hash_out_type,"hex") && dap_strcmp(l_hash_out_type,"base58")) {
+        dap_chain_node_cli_set_reply_text(a_str_reply, "invalid parameter -H, valid values: -H <hex | base58>");
+        return -1;
+    }
+
+    int l_cmd = CMD_NONE;
+    if (dap_chain_node_cli_find_option_val(a_argv, 1, 2, "list", NULL))
+        l_cmd = CMD_LIST;
+    else if (dap_chain_node_cli_find_option_val(a_argv, 1, 2, "tx", NULL))
+        l_cmd = CMD_TX_HISTORY;
+    // command tx_history
+    if(l_cmd == CMD_TX_HISTORY) {
+        bool l_is_all = dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-all", NULL);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-addr", &l_addr_base58);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-w", &l_wallet_name);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-net", &l_net_str);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-chain", &l_chain_str);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-tx", &l_tx_hash_str);
+
+        if(!l_is_all && !l_addr_base58 && !l_wallet_name && !l_tx_hash_str) {
+            dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-all' or '-addr' or '-w'");
+            return -1;
+        }
+
+        // Select chain network
+        if(!l_net_str) {
+            dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-net'");
+            return -2;
+        } else {
+            if((l_net = dap_chain_net_by_name(l_net_str)) == NULL) { // Can't find such network
+                dap_chain_node_cli_set_reply_text(a_str_reply,
+                        "command requires parameter '-net' to be valid chain network name");
+                return -3;
+            }
+        }
+        //Select chain emission
+        if(!l_chain_str) { // chain may be null -> then all chain use
+            //dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-chain'");
+            //return -4;
+        } else {
+            if((l_chain = dap_chain_net_get_chain_by_name(l_net, l_chain_str)) == NULL) { // Can't find such chain
+                dap_chain_node_cli_set_reply_text(a_str_reply,
+                        "command requires parameter '-chain' to be valid chain name in chain net %s",
+                        l_net_str);
+                return -5;
+            }
+        }
+        //char *l_group_mempool = dap_chain_net_get_gdb_group_mempool(l_chain);
+        //const char *l_chain_group = dap_chain_gdb_get_group(l_chain);
+
+        dap_chain_hash_fast_t l_tx_hash;
+        if(l_tx_hash_str) {
+            if(dap_chain_str_to_hash_fast(l_tx_hash_str, &l_tx_hash) < 0) {
+                l_tx_hash_str = NULL;
+                dap_chain_node_cli_set_reply_text(a_str_reply, "tx hash not recognized");
+                return -1;
+            }
+//        char hash_str[99];
+//        dap_chain_hash_fast_to_str(&l_tx_hash, hash_str,99);
+//        int gsdgsd=523;
+        }
+        dap_chain_addr_t *l_addr = NULL;
+        // if need addr
+        if(l_wallet_name || l_addr_base58) {
+            if(l_wallet_name) {
+                const char *c_wallets_path = dap_chain_wallet_get_path(g_config);
+                dap_chain_wallet_t * l_wallet = dap_chain_wallet_open(l_wallet_name, c_wallets_path);
+                if(l_wallet) {
+                    dap_chain_addr_t *l_addr_tmp = (dap_chain_addr_t *) dap_chain_wallet_get_addr(l_wallet,
+                            l_net->pub.id);
+                    l_addr = DAP_NEW_SIZE(dap_chain_addr_t, sizeof(dap_chain_addr_t));
+                    memcpy(l_addr, l_addr_tmp, sizeof(dap_chain_addr_t));
+                    dap_chain_wallet_close(l_wallet);
+                }
+            }
+            if(!l_addr && l_addr_base58) {
+                l_addr = dap_chain_addr_from_str(l_addr_base58);
+            }
+            if(!l_addr && !l_tx_hash_str) {
+                dap_chain_node_cli_set_reply_text(a_str_reply, "wallet address not recognized");
+                return -1;
+            }
+        }
+
+        dap_string_t *l_str_ret = dap_string_new(NULL); //char *l_str_ret = NULL;
+        dap_chain_t *l_chain_cur;
+        void *l_chain_tmp = (void*)0x1;
+        int l_num = 0;
+        // only one chain
+        if(l_chain)
+            l_chain_cur = l_chain;
+        // all chain
+        else
+            l_chain_cur = dap_chain_enum(&l_chain_tmp);
+        while(l_chain_cur) {
+            // only selected net
+            if(l_net->pub.id.uint64 == l_chain_cur->net_id.uint64) {
+                // separator between chains
+                if(l_num>0 && !l_chain)
+                    dap_string_append(l_str_ret, "---------------\n");
+
+                char *l_str_out = NULL;
+                dap_string_append_printf(l_str_ret, "chain: %s\n", l_chain_cur->name);
+                if(l_is_all) {
+                    // without filters
+                    l_str_out = dap_db_history_filter(l_chain_cur, NULL, l_hash_out_type);
+                    dap_string_append_printf(l_str_ret, "all history:\n%s\n", l_str_out ? l_str_out : " empty");
+                }
+                else {
+                    l_str_out = l_tx_hash_str ?
+                                                dap_db_history_tx(&l_tx_hash, l_chain_cur, l_hash_out_type) :
+                                                dap_db_history_addr(l_addr, l_chain_cur, l_hash_out_type);
+
+                    if(l_tx_hash_str) {
+                        dap_string_append_printf(l_str_ret, "history for tx hash %s:\n%s\n", l_tx_hash_str,
+                                l_str_out ? l_str_out : " empty");
+                    }
+                    else if(l_addr) {
+                        char *l_addr_str = dap_chain_addr_to_str(l_addr);
+                        dap_string_append_printf(l_str_ret, "history for addr %s:\n%s\n", l_addr_str,
+                                l_str_out ? l_str_out : " empty");
+                        DAP_DELETE(l_addr_str);
+                    }
+                }
+                DAP_DELETE(l_str_out);
+                l_num++;
+            }
+            // only one chain use
+            if(l_chain)
+                break;
+            dap_chain_enum_unlock();
+            l_chain_cur = dap_chain_enum(&l_chain_tmp);
+        }
+        // all chain
+        if(!l_chain)
+            dap_chain_enum_unlock();
+        dap_chain_node_cli_set_reply_text(a_str_reply, l_str_ret->str);
+        dap_string_free(l_str_ret, true);
+        return 0;
+    }
+    else{
+        dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter 'list' or 'tx' or 'info'");
+        return -1;
+    }
+}
+
+/**
+ * token command
+ *
+ */
+int com_token(int a_argc, char ** a_argv, void *a_arg_func, char **a_str_reply)
+{
+    enum { CMD_NONE, CMD_LIST, CMD_INFO, CMD_TX };
+    int arg_index = 1;
+    const char *l_addr_base58 = NULL;
+    const char *l_wallet_name = NULL;
+    const char *l_net_str = NULL;
+    const char *l_chain_str = NULL;
+
+    dap_chain_t * l_chain = NULL;
+    dap_chain_net_t * l_net = NULL;
+
+    const char * l_hash_out_type = NULL;
+    dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-H", &l_hash_out_type);
+    if(!l_hash_out_type)
+        l_hash_out_type = "base58";
+    if(dap_strcmp(l_hash_out_type,"hex") && dap_strcmp(l_hash_out_type,"base58")) {
+        dap_chain_node_cli_set_reply_text(a_str_reply, "invalid parameter -H, valid values: -H <hex | base58>");
+        return -1;
+    }
+
+    //bool l_is_all = dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-all", NULL);
+    //dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-addr", &l_addr_base58);
+    //dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-w", &l_wallet_name);
+    dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-net", &l_net_str);
+    //dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-chain", &l_chain_str);
+    //dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-tx", &l_tx_hash_str);
+
+    // Select chain network
+    if(!l_net_str) {
+        dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-net'");
+        return -2;
+    } else {
+        if((l_net = dap_chain_net_by_name(l_net_str)) == NULL) { // Can't find such network
+            dap_chain_node_cli_set_reply_text(a_str_reply,
+                    "command requires parameter '-net' to be valid chain network name");
+            return -3;
+        }
+    }
+
+    int l_cmd = CMD_NONE;
+    if (dap_chain_node_cli_find_option_val(a_argv, 1, 2, "list", NULL))
+        l_cmd = CMD_LIST;
+    else if (dap_chain_node_cli_find_option_val(a_argv, 1, 2, "info", NULL))
+        l_cmd = CMD_INFO;
+    else if (dap_chain_node_cli_find_option_val(a_argv, 1, 2, "tx", NULL))
+            l_cmd = CMD_TX;
+    // token list
+    if(l_cmd == CMD_LIST) {
+        dap_string_t *l_str_out = dap_string_new(NULL);
+        size_t l_token_num_total = 0;
+        // get first chain
+        void *l_chain_tmp = (void*)0x1;
+        dap_chain_t *l_chain_cur = dap_chain_enum(&l_chain_tmp);
+        while(l_chain_cur) {
+            // only selected net
+            if(l_net->pub.id.uint64 == l_chain_cur->net_id.uint64) {
+                size_t l_token_num = 0;
+                char *token_list_str = dap_db_history_token_list(l_chain_cur, NULL, l_hash_out_type, &l_token_num);
+                if(token_list_str)
+                    dap_string_append(l_str_out, token_list_str);
+                l_token_num_total += l_token_num;
+            }
+            // next chain
+            dap_chain_enum_unlock();
+            l_chain_cur = dap_chain_enum(&l_chain_tmp);
+        }
+        dap_chain_enum_unlock();
+        //total
+        dap_string_append_printf(l_str_out, "---------------\ntokens: %u\n", l_token_num_total);
+        dap_chain_node_cli_set_reply_text(a_str_reply, l_str_out->str);
+        dap_string_free(l_str_out, true);
+        return 0;
+
+    }
+    // token info
+    if(l_cmd == CMD_INFO) {
+        const char *l_token_name_str = NULL;
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-name", &l_token_name_str);
+        if(!l_token_name_str) {
+                dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-name' <token name>");
+                return -4;
+            }
+
+            dap_string_t *l_str_out = dap_string_new(NULL);
+            size_t l_token_num_total = 0;
+            // get first chain
+            void *l_chain_tmp = (void*)0x1;
+            dap_chain_t *l_chain_cur = dap_chain_enum(&l_chain_tmp);
+            while(l_chain_cur) {
+                // only selected net
+                if(l_net->pub.id.uint64 == l_chain_cur->net_id.uint64) {
+                    size_t l_token_num = 0;
+                    // filter - token name
+                    char *token_list_str = dap_db_history_token_list(l_chain_cur, l_token_name_str, l_hash_out_type, &l_token_num);
+                    if(token_list_str)
+                        dap_string_append(l_str_out, token_list_str);
+                    l_token_num_total += l_token_num;
+                }
+                // next chain
+                dap_chain_enum_unlock();
+                l_chain_cur = dap_chain_enum(&l_chain_tmp);
+            }
+            dap_chain_enum_unlock();
+            if(!l_token_num_total)
+                dap_string_append_printf(l_str_out, "token '%s' not found\n", l_token_name_str);
+            dap_chain_node_cli_set_reply_text(a_str_reply, l_str_out->str);
+            dap_string_free(l_str_out, true);
+            return 0;
+
+        }
+    // command tx history
+    if(l_cmd == CMD_TX) {
+
+        const char *l_token_name_str = NULL;
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-name", &l_token_name_str);
+        if(!l_token_name_str) {
+                dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-name' <token name>");
+                return -4;
+            }
+
+            dap_string_t *l_str_out = dap_string_new(NULL);
+            // get first chain
+            void *l_chain_tmp = (void*)0x1;
+            dap_chain_t *l_chain_cur = dap_chain_enum(&l_chain_tmp);
+            while(l_chain_cur) {
+                // only selected net
+                if(l_net->pub.id.uint64 == l_chain_cur->net_id.uint64) {
+                    char *token_list_str = dap_db_history_filter(l_chain_cur, l_token_name_str, l_hash_out_type);
+                    if(token_list_str){
+                        dap_string_append(l_str_out, "%s\n", token_list_str);
+                        dap_string_append(l_str_out, token_list_str);
+                }
+                // next chain
+                dap_chain_enum_unlock();
+                l_chain_cur = dap_chain_enum(&l_chain_tmp);
+            }
+            dap_chain_enum_unlock();
+            dap_chain_node_cli_set_reply_text(a_str_reply, l_str_out->str);
+            dap_string_free(l_str_out, true);
+            return 0;
+
+
+/*        bool l_is_all = dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-all", NULL);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-addr", &l_addr_base58);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-w", &l_wallet_name);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-net", &l_net_str);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-chain", &l_chain_str);
+        dap_chain_node_cli_find_option_val(a_argv, arg_index, a_argc, "-tx", &l_tx_hash_str);
+
+        if(!l_is_all && !l_addr_base58 && !l_wallet_name && !l_tx_hash_str) {
+            dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-all' or '-addr' or '-w'");
+            return -1;
+        }
+
+        // Select chain network
+        if(!l_net_str) {
+            dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-net'");
+            return -2;
+        } else {
+            if((l_net = dap_chain_net_by_name(l_net_str)) == NULL) { // Can't find such network
+                dap_chain_node_cli_set_reply_text(a_str_reply,
+                        "tx_history requires parameter '-net' to be valid chain network name");
+                return -3;
+            }
+        }
+        //Select chain emission
+        if(!l_chain_str) { // chain may be null -> then all chain use
+            //dap_chain_node_cli_set_reply_text(a_str_reply, "command requires parameter '-chain'");
+            //return -4;
+        } else {
+            if((l_chain = dap_chain_net_get_chain_by_name(l_net, l_chain_str)) == NULL) { // Can't find such chain
+                dap_chain_node_cli_set_reply_text(a_str_reply,
+                        "command requires parameter '-chain' to be valid chain name in chain net %s",
+                        l_net_str);
+                return -5;
+            }
+        }
+        */
+    }
+    return 0;
+}
+
+
diff --git a/modules/net/include/dap_chain_node_cli_cmd_tx.h b/modules/net/include/dap_chain_node_cli_cmd_tx.h
index 64fb1fbc0c..a7a1a4a317 100644
--- a/modules/net/include/dap_chain_node_cli_cmd_tx.h
+++ b/modules/net/include/dap_chain_node_cli_cmd_tx.h
@@ -31,3 +31,16 @@
  */
 char* dap_db_history_tx(dap_chain_hash_fast_t* a_tx_hash, dap_chain_t * a_chain, const char *a_hash_out_type);
 char* dap_db_history_addr(dap_chain_addr_t * a_addr, dap_chain_t * a_chain, const char *a_hash_out_type);
+
+/**
+ * ledger command
+ *
+ */
+int com_ledger(int a_argc, char ** a_argv, void *a_arg_func, char **a_str_reply);
+
+/**
+ * token command
+ *
+ */
+int com_token(int a_argc, char ** a_argv, void *a_arg_func, char **a_str_reply);
+
-- 
GitLab