public static void sendNickname(byte[] recipient, byte[] contact_address)
        {
            if (contact_address != null && contact_address.Length > 1)
            {
                if (!Node.users.hasUser(contact_address))
                {
                    return;
                }
                byte[] nickData = Node.users.getUser(contact_address).nickData;
                if (nickData != null)
                {
                    sendMessage(recipient, new StreamMessage(nickData));
                }

                return;
            }

            SpixiMessage reply_spixi_message = new SpixiMessage(SpixiMessageCode.nick, Encoding.UTF8.GetBytes(Config.botName));

            byte[] sender = IxianHandler.getWalletStorage().getPrimaryAddress();

            // Send the nickname message to friend
            StreamMessage reply_message = new StreamMessage();

            reply_message.type           = StreamMessageCode.info;
            reply_message.recipient      = recipient;
            reply_message.sender         = sender;
            reply_message.data           = reply_spixi_message.getBytes();
            reply_message.encryptionType = StreamMessageEncryptionCode.none;
            reply_message.id             = new byte[] { 5 };

            reply_message.sign(IxianHandler.getWalletStorage().getPrimaryPrivateKey());

            sendMessage(recipient, reply_message);
        }
        public static void sendAcceptAdd(byte[] recipient, byte[] pub_key)
        {
            Node.users.setPubKey(recipient, pub_key, false);
            var user = Node.users.getUser(recipient);

            if (user != null && user.status != BotContactStatus.banned)
            {
                user.status = BotContactStatus.normal;
            }

            SpixiMessage spixi_message = new SpixiMessage(SpixiMessageCode.acceptAddBot, null);

            StreamMessage message = new StreamMessage();

            message.type           = StreamMessageCode.info;
            message.recipient      = recipient;
            message.sender         = IxianHandler.getWalletStorage().getPrimaryAddress();
            message.data           = spixi_message.getBytes();
            message.encryptionType = StreamMessageEncryptionCode.none;
            message.id             = new byte[] { 1 };

            message.sign(IxianHandler.getWalletStorage().getPrimaryPrivateKey());

            sendMessage(recipient, message);
        }
        public static void onMsgDelete(byte[] msg_id, int channel, RemoteEndpoint endpoint)
        {
            StreamMessage msg = Messages.getMessage(msg_id, channel);

            if (msg == null)
            {
                return;
            }

            if (isAdmin(endpoint.presence.wallet) || msg.sender.SequenceEqual(endpoint.presence.wallet))
            {
                Messages.removeMessage(msg_id, channel);

                SpixiMessage spixi_message = new SpixiMessage(SpixiMessageCode.msgDelete, msg_id, channel);

                StreamMessage message = new StreamMessage();
                message.type           = StreamMessageCode.info;
                message.sender         = IxianHandler.getWalletStorage().getPrimaryAddress();
                message.recipient      = message.sender;
                message.data           = spixi_message.getBytes();
                message.encryptionType = StreamMessageEncryptionCode.none;

                message.sign(IxianHandler.getWalletStorage().getPrimaryPrivateKey());

                Messages.addMessage(message, channel, false);

                NetworkServer.forwardMessage(ProtocolMessageCode.s2data, message.getBytes());
            }
        }
        public static void sendAvatar(byte[] recipient, byte[] contact_address)
        {
            if (contact_address != null && contact_address.Length > 1)
            {
                if (!Node.users.hasUser(contact_address))
                {
                    return;
                }

                string path = Node.users.getAvatarPath(contact_address);
                if (path == null)
                {
                    return;
                }

                byte[] avatar_data = File.ReadAllBytes(path);
                if (avatar_data != null)
                {
                    sendMessage(recipient, new StreamMessage(avatar_data));
                }

                return;
            }

            byte[] avatar_bytes = Node.getAvatarBytes();

            if (avatar_bytes == null)
            {
                return;
            }

            SpixiMessage reply_spixi_message = new SpixiMessage(SpixiMessageCode.avatar, avatar_bytes);

            byte[] sender = IxianHandler.getWalletStorage().getPrimaryAddress();

            // Send the nickname message to friend
            StreamMessage reply_message = new StreamMessage();

            reply_message.type           = StreamMessageCode.info;
            reply_message.recipient      = recipient;
            reply_message.sender         = sender;
            reply_message.data           = reply_spixi_message.getBytes();
            reply_message.encryptionType = StreamMessageEncryptionCode.none;
            reply_message.id             = new byte[] { 6 };

            reply_message.sign(IxianHandler.getWalletStorage().getPrimaryPrivateKey());

            sendMessage(recipient, reply_message);
        }
        // Requests the avatar of the sender
        public static void requestAvatar(byte[] recipient)
        {
            // Prepare the message and send to the S2 nodes
            SpixiMessage spixi_message = new SpixiMessage(SpixiMessageCode.getAvatar, new byte[1]);

            StreamMessage message = new StreamMessage();

            message.type           = StreamMessageCode.info;
            message.recipient      = recipient;
            message.sender         = IxianHandler.getWalletStorage().getPrimaryAddress();
            message.data           = spixi_message.getBytes();
            message.encryptionType = StreamMessageEncryptionCode.none;
            message.id             = new byte[] { 4 };

            sendMessage(recipient, message);
        }
        public static void sendBotAction(byte[] recipient, SpixiBotActionCode action, byte[] data, int channel = 0)
        {
            SpixiBotAction sba = new SpixiBotAction(action, data);

            // Prepare the message and send to the S2 nodes
            SpixiMessage spixi_message = new SpixiMessage(SpixiMessageCode.botAction, sba.getBytes(), channel);

            StreamMessage message = new StreamMessage();

            message.type           = StreamMessageCode.info;
            message.recipient      = recipient;
            message.sender         = IxianHandler.getWalletStorage().getPrimaryAddress();
            message.data           = spixi_message.getBytes();
            message.encryptionType = StreamMessageEncryptionCode.none;

            sendMessage(recipient, message);
        }
        // Called when receiving S2 data from clients
        public static void receiveData(byte[] bytes, RemoteEndpoint endpoint)
        {
            // TODO Verify signature for all relevant messages

            string endpoint_wallet_string = Base58Check.Base58CheckEncoding.EncodePlain(endpoint.presence.wallet);

            Logging.info(string.Format("Receiving S2 data from {0}", endpoint_wallet_string));

            StreamMessage message = new StreamMessage(bytes);

            // Don't allow clients to send error stream messages, as it's reserved for S2 nodes only
            if (message.type == StreamMessageCode.error)
            {
                Logging.warn(string.Format("Discarding error message type from {0}", endpoint_wallet_string));
                return;
            }

            // Discard messages not sent to this node
            if (!IxianHandler.getWalletStorage().isMyAddress(message.recipient))
            {
                Logging.warn(string.Format("Discarding message that wasn't sent to this node from {0}", endpoint_wallet_string));
                return;
            }

            if (message.encryptionType != StreamMessageEncryptionCode.none && !message.decrypt(IxianHandler.getWalletStorage().getPrimaryPrivateKey(), null, null))
            {
                Logging.error("Could not decrypt message from {0}", Base58Check.Base58CheckEncoding.EncodePlain(message.sender));
                return;
            }

            SpixiMessage spixi_msg = new SpixiMessage(message.data);

            int channel = 0;

            if (spixi_msg != null)
            {
                channel = spixi_msg.channel;
            }

            if (message.requireRcvConfirmation)
            {
                switch (spixi_msg.type)
                {
                case SpixiMessageCode.msgReceived:
                case SpixiMessageCode.msgRead:
                case SpixiMessageCode.requestFileData:
                case SpixiMessageCode.fileData:
                case SpixiMessageCode.appData:
                case SpixiMessageCode.msgTyping:
                    // do not send received confirmation
                    break;

                case SpixiMessageCode.chat:
                    sendReceivedConfirmation(message.sender, message.id, channel, endpoint);
                    break;

                default:
                    sendReceivedConfirmation(message.sender, message.id, -1, endpoint);
                    break;
                }
            }

            switch (spixi_msg.type)
            {
            case SpixiMessageCode.requestAdd:
                // Friend request
                if (!new Address(spixi_msg.data).address.SequenceEqual(message.sender) || !message.verifySignature(spixi_msg.data))
                {
                    Logging.error("Unable to verify signature for message type: {0}, id: {1}, from: {2}.", message.type, Crypto.hashToString(message.id), Base58Check.Base58CheckEncoding.EncodePlain(message.sender));
                }
                else
                {
                    sendAcceptAdd(endpoint.presence.wallet, endpoint.presence.pubkey);
                    sendAvatar(endpoint.presence.wallet, null);
                }
                break;

            case SpixiMessageCode.getPubKey:
                if (Node.users.hasUser(spixi_msg.data))
                {
                    StreamMessage sm = new StreamMessage();
                    sm.type           = StreamMessageCode.info;
                    sm.sender         = IxianHandler.getWalletStorage().getPrimaryAddress();
                    sm.recipient      = message.sender;
                    sm.data           = new SpixiMessage(SpixiMessageCode.pubKey, Node.users.getUser(spixi_msg.data).publicKey).getBytes();
                    sm.encryptionType = StreamMessageEncryptionCode.none;

                    sendMessage(endpoint.presence.wallet, sm);
                }
                break;

            case SpixiMessageCode.getNick:
                sendNickname(endpoint.presence.wallet, spixi_msg.data);
                break;

            case SpixiMessageCode.getAvatar:
                sendAvatar(endpoint.presence.wallet, spixi_msg.data);
                break;

            case SpixiMessageCode.nick:
                Node.users.setPubKey(endpoint.presence.wallet, endpoint.serverPubKey, false);
                Node.users.setNick(endpoint.presence.wallet, message.getBytes());
                break;

            case SpixiMessageCode.avatar:
                Node.users.setPubKey(endpoint.presence.wallet, endpoint.serverPubKey, false);
                if (message.data.Length < 500000)
                {
                    if (message.data == null)
                    {
                        Node.users.setAvatar(endpoint.presence.wallet, null);
                    }
                    else
                    {
                        Node.users.setAvatar(endpoint.presence.wallet, message.getBytes());
                    }
                }
                break;

            case SpixiMessageCode.chat:
                onChat(bytes, message, channel, endpoint);
                break;

            case SpixiMessageCode.botGetMessages:
                Messages.sendMessages(endpoint.presence.wallet, channel, spixi_msg.data);
                break;

            case SpixiMessageCode.msgReceived:
            {
                // don't send confirmation back, so just return
                return;
            }

            case SpixiMessageCode.msgRead:
            {
                // don't send confirmation back, so just return
                return;
            }

            case SpixiMessageCode.botAction:
                onBotAction(spixi_msg.data, endpoint);
                break;

            case SpixiMessageCode.msgDelete:
                onMsgDelete(spixi_msg.data, channel, endpoint);
                break;

            case SpixiMessageCode.msgReaction:
                onMsgReaction(message, spixi_msg.data, channel, endpoint);
                break;

            case SpixiMessageCode.leave:
                onLeave(message.sender);

                break;

            default:
                Logging.warn("Received message type that isn't handled {0}", spixi_msg.type);
                break;
            }

            // TODO: commented for development purposes ONLY!

            /*
             *          // Extract the transaction
             *          Transaction transaction = new Transaction(message.transaction);
             *
             *          // Validate transaction sender
             *          if(transaction.from.SequenceEqual(message.sender) == false)
             *          {
             *              Logging.error(string.Format("Relayed message transaction mismatch for {0}", endpoint_wallet_string));
             *              sendError(message.sender);
             *              return;
             *          }
             *
             *          // Validate transaction amount and fee
             *          if(transaction.amount < CoreConfig.relayPriceInitial || transaction.fee < CoreConfig.transactionPrice)
             *          {
             *              Logging.error(string.Format("Relayed message transaction amount too low for {0}", endpoint_wallet_string));
             *              sendError(message.sender);
             *              return;
             *          }
             *
             *          // Validate transaction receiver
             *          if (transaction.toList.Keys.First().SequenceEqual(IxianHandler.getWalletStorage().address) == false)
             *          {
             *              Logging.error("Relayed message transaction receiver is not this S2 node");
             *              sendError(message.sender);
             *              return;
             *          }
             *
             *          // Update the recipient dictionary
             *          if (dataRelays.ContainsKey(message.recipient))
             *          {
             *              dataRelays[message.recipient]++;
             *              if(dataRelays[message.recipient] > Config.relayDataMessageQuota)
             *              {
             *                  Logging.error(string.Format("Exceeded amount of unpaid data relay messages for {0}", endpoint_wallet_string));
             *                  sendError(message.sender);
             *                  return;
             *              }
             *          }
             *          else
             *          {
             *              dataRelays.Add(message.recipient, 1);
             *          }
             *
             *
             *          // Store the transaction
             *          StreamTransaction streamTransaction = new StreamTransaction();
             *          streamTransaction.messageID = message.getID();
             *          streamTransaction.transaction = transaction;
             *          lock (transactions)
             *          {
             *              transactions.Add(streamTransaction);
             *          }
             *
             *          // For testing purposes, allow the S2 node to receive relay data itself
             *          if (message.recipient.SequenceEqual(IxianHandler.getWalletStorage().getWalletAddress()))
             *          {
             *              string test = Encoding.UTF8.GetString(message.data);
             *              Logging.info(test);
             *
             *              return;
             *          }
             *
             *          Logging.info("NET: Forwarding S2 data");
             *          NetworkStreamServer.forwardMessage(message.recipient, DLT.Network.ProtocolMessageCode.s2data, bytes);
             */
        }