<filename>txtenna.py
""" txtenna.py - 
This code is derived from sample.py available from the goTenna Public SDK (https://github.com/gotenna/PublicSDK)
"""
from __future__ import print_function
import cmd # for the command line application
import sys # To quit
import os
import traceback
import logging
import requests
import json
from threading import Thread
from time import sleep
import random
import string
import binascii
import goTenna # The goTenna API
from segment_storage import SegmentStorage
from txtenna_segment import TxTennaSegment
from io import BytesIO
import httplib
import struct
import zlib

# For SPI connection only, set SPI_CONNECTION to true with proper SPI settings
SPI_CONNECTION = False
SPI_BUS_NO = 0
SPI_CHIP_NO = 0
SPI_REQUEST = 22
SPI_READY = 27

# Configure the Python logging module to print to stderr. In your application,
# you may want to route the logging elsewhere.
logging.basicConfig()

# Import readline if the system has it
try:
    import readline
    assert readline # silence pyflakes
except ImportError:
    pass

# Import support for bitcoind RPC interface
import bitcoin
import bitcoin.rpc
from bitcoin.core import x, lx, b2x, b2lx, CMutableTxOut, CMutableTransaction
from bitcoin.wallet import CBitcoinAddress
bitcoin.SelectParams('mainnet')

TXTENNA_GATEWAY_GID = 2573394689

class goTennaCLI(cmd.Cmd):
    """ CLI handler function
    """
    def __init__(self):
        self.api_thread = None
        self.status = {}
        cmd.Cmd.__init__(self)
        self.prompt = 'txTenna>'
        self.in_flight_events = {}
        self._set_frequencies = False
        self._set_tx_power = False
        self._set_bandwidth = False
        self._set_geo_region = False
        self._settings = goTenna.settings.GoTennaSettings(
            rf_settings=goTenna.settings.RFSettings(), 
            geo_settings=goTenna.settings.GeoSettings())
        self._do_encryption = True
        self._awaiting_disconnect_after_fw_update = [False]
        self.messageIdx = 0
        self.local = False
        self.segment_storage = SegmentStorage()
        self.send_dir = None
        self.receive_dir = None
        self.watch_dir_thread = None
        self.pipe_file = None

    def precmd(self, line):
        if not self.api_thread\
           and not line.startswith('sdk_token')\
           and not line.startswith('quit'):
            print("An SDK token must be entered to begin.")
            return ''
        return line

    def do_sdk_token(self, rst):
        """ Enter an SDK token to begin usage of the driver. Usage: sdk_token TOKEN"""
        if self.api_thread:
            print("To change SDK tokens, restart the sample app.")
            return
        try:
            if not SPI_CONNECTION:
                self.api_thread = goTenna.driver.Driver(sdk_token=rst, gid=None, 
                                                    settings=None, 
                                                    event_callback=self.event_callback)
            else:
                self.api_thread = goTenna.driver.SpiDriver(
                                    SPI_BUS_NO, SPI_CHIP_NO, 22, 27,
                                    rst, None, None, self.event_callback)
            self.api_thread.start()
        except ValueError:
            print("SDK token {} is not valid. Please enter a valid SDK token."
                  .format(rst))

    def emptyline(self):
        pass

    def event_callback(self, evt):
        """ The event callback that will print messages from the API.

        See the documentation for ``goTenna.driver``.

        This will be invoked from the API's thread when events are received.
        """
        if evt.event_type == goTenna.driver.Event.MESSAGE:
            try:
                ## print(str(evt))
                self.handle_message(evt.message)
            except Exception:
                traceback.print_exc()
        elif evt.event_type == goTenna.driver.Event.DEVICE_PRESENT:
            ## print(str(evt))
            if self._awaiting_disconnect_after_fw_update[0]:
                print("Device physically connected")
            else:
                print("Device physically connected, configure to continue")
        elif evt.event_type == goTenna.driver.Event.CONNECT:
            if self._awaiting_disconnect_after_fw_update[0]:
                print("Device reconnected! Firmware update complete!")
                self._awaiting_disconnect_after_fw_update[0] = False
            else:
                print("Connected!")
                print(str(evt))
        elif evt.event_type == goTenna.driver.Event.DISCONNECT:
            if self._awaiting_disconnect_after_fw_update[0]:
                # Do not reset configuration so that the device will reconnect on its own
                print("Firmware update: Device disconnected, awaiting reconnect")
            else:
                print("Disconnected! {}".format(evt))
                # We reset the configuration here so that if the user plugs in a different
                # device it is not immediately reconfigured with new and incorrect data
                self.api_thread.set_gid(None)
                self.api_thread.set_rf_settings(None)
                self._set_frequencies = False
                self._set_tx_power = False
                self._set_bandwidth = False
        elif evt.event_type == goTenna.driver.Event.STATUS:
            self.status = evt.status
        elif evt.event_type == goTenna.driver.Event.GROUP_CREATE:
            index = -1
            for idx, member in enumerate(evt.group.members):
                if member.gid_val == self.api_thread.gid.gid_val:
                    index = idx
                    break
            print("Added to group {}: You are member {}"
                  .format(evt.group.gid.gid_val,
                          index))

    def build_callback(self, error_handler=None):
        """ Build a callback for sending to the API thread. May speciy a callable
        error_handler(details) taking the error details from the callback. The handler should return a string.
        """
        def default_error_handler(details):
            """ Easy error handler if no special behavior is needed. Just builds a string with the error.
            """
            if details['code'] in [goTenna.constants.ErrorCodes.TIMEOUT,
                                   goTenna.constants.ErrorCodes.OSERROR,
                                   goTenna.constants.ErrorCodes.EXCEPTION]:
                return "USB connection disrupted"
            return "Error: {}: {}".format(details['code'], details['msg'])
        # Define a second function here so it implicitly captures self
        captured_error_handler = [error_handler]
        def callback(correlation_id, success=None, results=None,
                     error=None, details=None):
            """ The default callback to pass to the API.

            See the documentation for ``goTenna.driver``.

            Does nothing but print whether the method succeeded or failed.
            """
            method = self.in_flight_events.pop(correlation_id.bytes,
                                               'Method call')
            if success:
                if results:
                    print("{} succeeded: {}".format(method, results))
                else:
                    print("{} succeeded!".format(method))
            elif error:
                if not captured_error_handler[0]:
                    captured_error_handler[0] = default_error_handler
                print("{} failed: {}".format(method,
                                             captured_error_handler[0](details)))
        return callback

    def do_set_gid(self, rem):
        """ Create a new profile (if it does not already exist) with default settings.

        Usage: set_gid GID

        GID should be a 15-digit numerical GID.
        """
        if self.api_thread.connected:
            print("Must not be connected when setting GID")
            return
        (gid, _) = self._parse_gid(rem, goTenna.settings.GID.PRIVATE)
        if not gid:
            return
        self.api_thread.set_gid(gid)

    def do_quit(self, arg):
        """ Safely quit.

        Usage: quit
        """
        # pylint: disable=unused-argument
        if self.api_thread:
            self.api_thread.join()
        sys.exit()

    def do_send_broadcast(self, message):
        """ Send a broadcast message

        Usage: send_broadcast MESSAGE
        """
        if not self.api_thread.connected:
            print("No device connected")
        else:
            def error_handler(details):
                """ A special error handler for formatting message failures
                """
                if details['code'] in [goTenna.constants.ErrorCodes.TIMEOUT,
                                       goTenna.constants.ErrorCodes.OSERROR]:
                    return "Message may not have been sent: USB connection disrupted"
                return "Error sending message: {}".format(details)
            try:
                method_callback = self.build_callback(error_handler)
                payload = goTenna.payload.TextPayload(message)
                print("payload valid = {}, message size = {}\n".format(payload.valid, len(message)))

                corr_id = self.api_thread.send_broadcast(payload, method_callback)
                while (corr_id is None):
                    ## try again if send_broadcast fails
                    sleep(10)
                    corr_id = self.api_thread.send_broadcast(payload, method_callback)

                self.in_flight_events[corr_id.bytes] = 'Broadcast message: {} ({} bytes)\n'.format(message,len(message))
            except ValueError:
                print("Message too long!")
                return

    @staticmethod
    def _parse_gid(line, gid_type, print_message=True):
        parts = line.split(' ')
        remainder = ' '.join(parts[1:])
        gidpart = parts[0]
        try:
            gid = int(gidpart)
            if gid > goTenna.constants.GID_MAX:
                print('{} is not a valid GID. The maximum GID is {}'
                      .format(str(gid), str(goTenna.constants.GID_MAX)))
                return (None, line)
            gidobj = goTenna.settings.GID(gid, gid_type)
            return (gidobj, remainder)
        except ValueError:
            if print_message:
                print('{} is not a valid GID.'.format(line))
            return (None, remainder)

    def do_send_private(self, rem):
        """ Send a private message to a contact

        Usage: send_private GID MESSAGE

        GID is the GID to send the private message to.

        MESSAGE is the message.
        """
        if not self.api_thread.connected:
            print("Must connect first")
            return
        (gid, rest) = self._parse_gid(rem, goTenna.settings.GID.PRIVATE)
        if not gid:
            return
        message = rest
        def error_handler(details):
            """ Special error handler for sending private messages to format errors
            """
            return "Error sending message: {}".format(details)

        try:
            method_callback = self.build_callback(error_handler)
            payload = goTenna.payload.TextPayload(message)
            def ack_callback(correlation_id, success):
                if success:
                    print("Private message to {}: delivery confirmed"
                          .format(gid.gid_val))
                else:
                    print("Private message to {}: delivery not confirmed, recipient may be offline or out of range"
                          .format(gid.gid_val))
            corr_id = self.api_thread.send_private(gid, payload,
                                                   method_callback,
                                                   ack_callback=ack_callback,
                                                   encrypt=self._do_encryption)
        except ValueError:
            print("Message too long!")
            return
        self.in_flight_events[corr_id.bytes]\
            = 'Private message to {}: {}'.format(gid.gid_val, message)

    def get_device_type(self):
        return self.api_thread.device_type

    def do_list_geo_region(self, rem):
        """ List the available region.

        Usage: list_geo_region
        """
        print("Allowed region:")
        for region in goTenna.constants.GEO_REGION.DICT:
            print("region {} : {}"
                  .format(region, goTenna.constants.GEO_REGION.DICT[region]))

    def do_set_geo_region(self, rem):
        """ Configure the frequencies the device will use.

        Usage: set_geo_region REGION

        Allowed region displayed with list_geo_region.
        """
        if self.get_device_type() == "pro":
            print("This configuration cannot be done for Pro devices.")
            return
        region = int(rem.strip())
        print('region={}'.format(region))
        if not goTenna.constants.GEO_REGION.valid(region):
            print("Invalid region setting {}".format(rem))
            return
        self._set_geo_region = True
        self._settings.geo_settings.region = region
        self.api_thread.set_geo_settings(self._settings.geo_settings)

    def do_can_connect(self, rem):
        """ Return whether a goTenna can connect. For a goTenna to connect, a GID and RF settings must be configured.
        """
        # pylint: disable=unused-argument
        if self.api_thread.gid:
            print("GID: OK")
        else:
            print("GID: Not Set")
        if self._set_tx_power:
            print("PRO - TX Power: OK")
        else:
            print("PRO - TX Power: Not Set")
        if self._set_frequencies:
            print("PRO - Frequencies: OK")
        else:
            print("PRO - Frequencies: Not Set")
        if self._set_bandwidth:
            print("PRO - Bandwidth: OK")
        else:
            print("PRO - Bandwidth: Not Set")
        if self._set_geo_region:
            print("MESH - Geo region: OK")
        else:
            print("MESH - Geo region: Not Set")

    def do_get_system_info(self, args):
        """ Get system information.

        Usage: get_system_info
        """
        if not self.api_thread.connected:
            print("Device must be connected")
        print(self.api_thread.system_info)

    def do_rpc_getrawtransaction(self, tx_id) :
        """
        Call local Bitcoin RPC method 'getrawtransaction'

        Usage: rpc_sendtoaddress TX_ID
        """
        try :
            proxy = bitcoin.rpc.Proxy()
            r = proxy.getrawtransaction(lx(tx_id), True)
            print(str(r))
        except:
            traceback.print_exc()

    def confirm_bitcoin_tx_local(self, hash, sender_gid):
        """ 
        Confirm bitcoin transaction using local bitcoind instance

        Usage: confirm_bitcoin_tx tx_id gid
        """ 

        ## send transaction to local bitcond
        segments = self.segment_storage.get_by_transaction_id(hash)
        raw_tx = self.segment_storage.get_raw_tx(segments)

        ## pass hex string converted to bytes
        try :
            proxy1 = bitcoin.rpc.Proxy()
            raw_tx_bytes = x(raw_tx)
            tx = CMutableTransaction.stream_deserialize(BytesIO(raw_tx_bytes))
            r1 = proxy1.sendrawtransaction(tx)
        except :
            print("Invalid Transaction! Could not send to network.")
            return

        ## try for 30 minutes to confirm the transaction
        for n in range(0, 30) :
            try :
                proxy2 = bitcoin.rpc.Proxy()
                r2 = proxy2.getrawtransaction(r1, True)

                ## send zero-conf message back to tx sender
                confirmations = r2.get('confirmations', 0)
                rObj = TxTennaSegment('', '', tx_hash=hash, block=confirmations)
                arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
                self.do_send_private(arg)

                print("\nSent to GID: " + str(sender_gid) + ": Transaction " + hash + " added to the mempool.")
                break      
            except IndexError:
                ## tx_id not yet in the global mempool, sleep for a minute and then try again
                sleep(60)
                continue      
            
            ## wait for atleast one confirmation
            for m in range(0, 30):
                sleep(60) # sleep for a minute
                try :
                    proxy3= bitcoin.rpc.Proxy()
                    r3 = proxy3.getrawtransaction(r1, True)
                    confirmations = r3.get('confirmations', 0)
                    ## keep waiting until 1 or more confirmations
                    if confirmations > 0:
                        break
                except :
                    ## unknown RPC error, but keep trying
                    traceback.print_exc()

            if confirmations > 0 :
                ## send confirmations message back to tx sender if confirmations > 0
                rObj = TxTennaSegment('', '', tx_hash=hash, block=confirmations)
                arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
                self.do_send_private(arg)
                print("\nSent to GID: " + str(sender_gid) + ", Transaction " + hash + " confirmed in " + str(confirmations) + " blocks.")
            else :
                print("\CTransaction from GID: " + str(sender_gid) + ", Transaction " + hash + " not confirmed after 30 minutes.")

    def confirm_bitcoin_tx_online(self, hash, sender_gid, network):
        """ confirm bitcoin transaction using default online Samourai API instance

        Usage: confirm_bitcoin_tx tx_id gid network
        """

        if  network == 't' :
            url = "https://api.samourai.io/test/v2/tx/" + hash ## default testnet txtenna-server
        else :
            url = "https://api.samourai.io/v2/tx/" + hash ## default txtenna-server
        
        try:
            r = requests.get(url)
            ## print(r.text)

            while r.status_code != 200:
                sleep(60) # sleep for a minute
                r = requests.get(url)

            ## send zero-conf message back to tx sender
            rObj = TxTennaSegment('', '', tx_hash=hash, block=0)
            arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
            self.do_send_private(arg)    

            print("\nSent to GID: " + str(sender_gid) + ": Transaction " + hash + " added to the mempool.")            

            r_text = "".join(r.text.split()) # remove whitespace
            obj = json.loads(r_text)
            while not 'block' in obj.keys():
                sleep(60) # sleep for a minute
                r = requests.get(url)
                r_text = "".join(r.text.split())
                obj = json.loads(r_text)

            ## send block height message back to tx sender
            blockheight = obj['block']['height']
            rObj = TxTennaSegment('', '', tx_hash=hash, block=blockheight)
            arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
            self.do_send_private(arg)

            print("\nSent to GID: " + str(sender_gid) + ": Transaction " + hash + " confirmed in block " + str(blockheight) + ".")

        except:
            traceback.print_exc()

    def create_output_data_struct(self, data):
        """Create the output data structure generated by the blocksat receiver

        The "Protocol Sink" block of the blocksat-rx application places the incoming
        API data into output structures. This function creates the exact same
        structure that the blocksat-rx application would.

        Args:
            data : Sequence of bytes to be placed in the output structure

        Returns:
            Output data structure as sequence of bytes

        """

        # Header of the output data structure that the Blockstream Satellite Receiver
        # generates prior to writing user data into the API named pipe
        OUT_DATA_HEADER_FORMAT     = '64sQ'
        OUT_DATA_DELIMITER         = 'vyqzbefrsnzqahgdkrsidzigxvrppato' + \
                             '\xe0\xe0$\x1a\xe4["\xb5Z\x0bv\x17\xa7\xa7\x9d' + \
                             '\xa5\xd6\x00W}M\xa6TO\xda7\xfaeu:\xac\xdc'

        # Struct is composed of a delimiter and the message length
        out_data_header = struct.pack(OUT_DATA_HEADER_FORMAT,
                                    OUT_DATA_DELIMITER,
                                    len(data))

        # Final output data structure
        out_data = out_data_header + data

        return out_data            
            
    def receive_message_from_gateway(self, filename):
        """ 
        Receive message data from a mesh gateway node

        Usage: receive_message_from_gateway filename
        """ 

        ## send transaction to local blocksat reader pipe
        segments = self.segment_storage.get_by_transaction_id(filename)
        raw_data = self.segment_storage.get_raw_tx(segments).encode("utf-8")

        decoded_data = zlib.decompress(raw_data.decode('base64'))

        deliminted_data = self.create_output_data_struct(decoded_data)

        ## send the data to the blocksat pipe
        try :
            print("Message Data received for [" + filename + "] ( " + str(len(decoded_data)) + " bytes ) :\n" + str(decoded_data) + "\n")
        except UnicodeDecodeError :
            print("Binary Data received for [" + filename + "] ( " + str(len(decoded_data)) + " bytes )\n")
        
        if not self.pipe_file is None and os.path.exists(self.pipe_file) is True :
            # Open pipe and write raw data to it
            pipe_f = os.open(self.pipe_file, os.O_RDWR)
            os.write(pipe_f, deliminted_data)
        elif not self.receive_dir is None and os.path.exists(self.receive_dir) is True :
            # Create file
            dump_f = os.open(os.path.join(self.receive_dir, filename), os.O_CREAT | os.O_RDWR)
            os.write(dump_f, decoded_data)
        else :
            print("ERROR: Could not save data. No pipe found at [" + self.pipe_file + "] and no receive directory found at [" + self.receive_dir +"]\n")

    def handle_message(self, message):
        """ handle a txtenna message received over the mesh network

        Usage: handle_message message
        """
        payload = str(message.payload.message)
        print("received transaction payload: " + payload)

        segment = TxTennaSegment.deserialize_from_json(payload)
        self.segment_storage.put(segment)
        network = self.segment_storage.get_network(segment.payload_id)

        ## process incoming transaction confirmation from another server
        if (segment.block > 0):
            print("\nTransaction " + segment.payload_id + " confirmed in block " + str(segment.block))
        elif (segment.block is 0):
            print("\nTransaction " + segment.payload_id + " added to the the mem pool")
        elif (network is 'd'):
            ## process message data
            if (self.segment_storage.is_complete(segment.payload_id)):
                filename = self.segment_storage.get_transaction_id(segment.payload_id)
                t = Thread(target=self.receive_message_from_gateway, args=(filename,))
                t.start()
        else:
            ## process incoming tx segment
            if not self.local :
                headers = {u'content-type': u'application/json'}
                url = "https://api.samouraiwallet.com/v2/txtenna/segments" ## default txtenna-server
                r = requests.post(url, headers= headers, data=payload)
                print(r.text)

            if (self.segment_storage.is_complete(segment.payload_id)):
                sender_gid = message.sender.gid_val
                tx_id = self.segment_storage.get_transaction_id(segment.payload_id)

                ## check for confirmed transaction in a new thread
                if (self.local) :
                    t = Thread(target=self.confirm_bitcoin_tx_local, args=(tx_id, sender_gid))
                else :
                    t = Thread(target=self.confirm_bitcoin_tx_online, args=(tx_id, sender_gid, network))
                t.start()

    def do_mesh_broadcast_rawtx(self, rem):
        """ 
        Broadcast the raw hex of a Bitcoin transaction and its transaction ID over mainnet or testnet. 
        A local copy of txtenna-server must be configured to support the selected network.

        Usage: mesh_broadcast_tx RAW_HEX TX_ID NETWORK(m|t)

        eg. txTenna> mesh_broadcast_rawtx 01000000000101bf6c3ed233e8700b42c1369993c2078780015bab7067b9751b7f49f799efbffd0000000017160014f25dbf0eab0ba7e3482287ebb41a7f6d361de6efffffffff02204e00000000000017a91439cdb4242013e108337df383b1bf063561eb582687abb93b000000000017a9148b963056eedd4a02c91747ea667fc34548cab0848702483045022100e92ce9b5c91dbf1c976d10b2c5ed70d140318f3bf2123091d9071ada27a4a543022030c289d43298ca4ca9d52a4c85f95786c5e27de5881366d9154f6fe13a717f3701210204b40eff96588033722f487a52d39a345dc91413281b31909a4018efb330ba2600000000 94406beb94761fa728a2cde836ca636ecd3c51cbc0febc87a968cb8522ce7cc1 m
        """

        ## TODO: test Z85 binary encoding and add as an option
        (strHexTx, strHexTxHash, network) = rem.split(" ")
        gid = self.api_thread.gid.gid_val
        segments = TxTennaSegment.tx_to_segments(gid, strHexTx, strHexTxHash, str(self.messageIdx), network, False)
        for seg in segments :
            self.do_send_broadcast(seg.serialize_to_json())
            sleep(10)
        self.messageIdx = (self.messageIdx+1) % 9999

    def do_rpc_getbalance(self, rem) :
        """
        Call local Bitcoin RPC method 'getbalance'

        Usage: rpc_getbalance
        """
        try :
            proxy = bitcoin.rpc.Proxy()
            balance = proxy.getbalance()
            print("getbalance: " + str(balance))
        except Exception: # pylint: disable=broad-except
            traceback.print_exc()     

    def do_rpc_sendrawtransaction(self, hex) :
        """
        Call local Bitcoin RPC method 'sendrawtransaction'

        Usage: rpc_sendrawtransaction RAW_TX_HEX
        """
        try :
            proxy = bitcoin.rpc.Proxy()
            r = proxy.sendrawtransaction(hex)
            print("sendrawtransaction: " + str(r))
        except Exception: # pylint: disable=broad-except
            traceback.print_exc()

    def do_rpc_sendtoaddress(self, rem) :
        """
        Call local Bitcoin RPC method 'sendtoaddress'

        Usage: rpc_sendtoaddress ADDRESS SATS
        """
        try:
            proxy = bitcoin.rpc.Proxy()
            (addr, amount) = rem.split()
            r = proxy.sendtoaddress(addr, amount)
            print("sendtoaddress, transaction id: " + str(r["hex"]))
        except Exception: # pylint: disable=broad-except
            traceback.print_exc() 

    def do_mesh_sendtoaddress(self, rem) :
        """ 
        Create a signed transaction and broadcast it over the connected mesh device. The transaction 
        spends some amount of satoshis to the specified address from the local bitcoind wallet and selected network. 

        Usage: mesh_sendtoaddress ADDRESS SATS NETWORK(m|t)

        eg. txTenna> mesh_sendtoaddress 2N4BtwKZBU3kXkWT7ZBEcQLQ451AuDWiau2 13371337 t
        """
        try:

            proxy = bitcoin.rpc.Proxy()
            (addr, sats, network) = rem.split()

            # Create the txout. This time we create the scriptPubKey from a Bitcoin
            # address.
            txout = CMutableTxOut(sats, CBitcoinAddress(addr).to_scriptPubKey())

            # Create the unsigned transaction.
            unfunded_transaction = CMutableTransaction([], [txout])
            funded_transaction = proxy.fundrawtransaction(unfunded_transaction)
            signed_transaction = proxy.signrawtransaction(funded_transaction["tx"])
            txhex = b2x(signed_transaction["tx"].serialize())
            txid = b2lx(signed_transaction["tx"].GetTxid())
            print("sendtoaddress_mesh (tx, txid, network): " + txhex + ", " + txid, ", " + network)

            # broadcast over mesh
            self.do_mesh_broadcast_rawtx( txhex + " " + txid + " " + network)

        except Exception: # pylint: disable=broad-except
            traceback.print_exc()

        try :
            # lock UTXOs used to fund the tx if broadcast successful
            vin_outpoints = set()
            for txin in funded_transaction["tx"].vin:
                vin_outpoints.add(txin.prevout)
            ## json_outpoints = [{'txid':b2lx(outpoint.hash), 'vout':outpoint.n}
            ##              for outpoint in vin_outpoints]
            ## print(str(json_outpoints))
            proxy.lockunspent(False, vin_outpoints)
            
        except Exception: # pylint: disable=broad-except
            ## TODO: figure out why this is happening
            print("RPC timeout after calling lockunspent")

    def do_broadcast_messages(self, send_dir) :
        """ 
        Watch a particular directory for files with message data to be broadcast over the mesh network

        Usage: broadcast_messages DIRECTORY

        eg. txTenna> broadcast_messages ./downloads
        """

        if (send_dir is not None):
            #start new thread to watch directory
            self.watch_dir_thread = Thread(target=self.watch_messages, args=(send_dir,))
            self.watch_dir_thread.start()

    def watch_messages(self, send_dir):
        
        before = {}
        while os.path.exists(send_dir):
            sleep (10)
            after = dict ([(f, None) for f in os.listdir (send_dir)])
            new_files = [f for f in after if not f in before]
            if new_files:
                self.broadcast_message_files(send_dir, new_files)
            before = after

    def broadcast_message_files(self, directory, filenames):
        for filename in filenames:
            print("Broadcasting ",directory+"/"+filename)
            f = open(directory+"/"+filename,'r')
            message_data = f.read()
            f.close
            
            ## binary to ascii encoding and strip out newlines
            encoded = zlib.compress(message_data, 9).encode('base64').replace('\n','')
            print("[\n" + encoded.decode() + "\n]")

            gid = self.api_thread.gid.gid_val
            segments = TxTennaSegment.tx_to_segments(gid, encoded, filename, str(self.messageIdx), "d", False)
            for seg in segments :
                self.do_send_broadcast(seg.serialize_to_json())
                sleep(10)
            self.messageIdx = (self.messageIdx+1) % 9999


def run_cli():
    """
    The main function of the sample app.

    Instantiates a CLI object and runs it.
    """
    cli_obj = goTennaCLI()

    import argparse
    import six

    parser = argparse.ArgumentParser('Run a txTenna transaction gateway')
    parser.add_argument('SDK_TOKEN', type=six.b,
                        help='The token for the goTenna SDK')
    parser.add_argument('GEO_REGION', type=six.b,
                        help='The geo region number you are in')
    parser.add_argument("--gateway", action="store_true",
                        help="Use this computer as an internet connected transaction gateway with a default GID")
    parser.add_argument("--local", action="store_true",
                        help="Use local bitcoind to confirm and broadcast transactions")
    parser.add_argument("--send_dir",
                        help="Broadcast message data from files in this directory")
    parser.add_argument("--receive_dir",
                        help="Write files from received message data in this directory")
    parser.add_argument('-p', '--pipe',
                        default='/tmp/blocksat/api',
                        help='Pipe on which relayed message data is written out to ' +
                        '(default: /tmp/blocksat/api)')
    args = parser.parse_args()  

    ## start goTenna SDK thread by setting the SDK token
    cli_obj.do_sdk_token(args.SDK_TOKEN)

    ## set geo region
    cli_obj.do_set_geo_region(args.GEO_REGION)

    if args.gateway :
        ## use default gateway GID
        _gid = str(TXTENNA_GATEWAY_GID)
    else :
        ## create a random GID to use for sending transaction
        _gid = ''.join(random.SystemRandom().choice(string.hexdigits) for _ in range(12))
        _gid = str(int(_gid, 16))

    cli_obj.do_set_gid(_gid)
    print("set gid=",_gid)

    ## use local bitcoind to confirm transactions if 'local' is true
    cli_obj.local = args.local

    ## broadcast message data from files in this directory, eg. created by the blocksat
    cli_obj.send_dir = args.send_dir
    if (args.send_dir is not None):
        cli_obj.do_broadcast_messages(args.send_dir)

    cli_obj.pipe_file = args.pipe
    cli_obj.receive_dir = args.receive_dir

    try:
        sleep(5)
        cli_obj.cmdloop("Welcome to the txTenna API sample! "
                        "Press ? for a command list.\n")
    except Exception: # pylint: disable=broad-except
        traceback.print_exc()
        cli_obj.do_quit('')

if __name__ == '__main__':
    run_cli()

<filename>txtenna.py
""" txtenna.py - 
This code is derived from sample.py available from the goTenna Public SDK (https://github.com/gotenna/PublicSDK)
"""
from __future__ import print_function
import cmd # for the command line application
import sys # To quit
import os
import traceback
import logging
import requests
import json
from threading import Thread
from time import sleep
import random
import string
import binascii
import goTenna # The goTenna API
from segment_storage import SegmentStorage
from txtenna_segment import TxTennaSegment
from io import BytesIO
import httplib
import struct
import zlib

# For SPI connection only, set SPI_CONNECTION to true with proper SPI settings
SPI_CONNECTION = False
SPI_BUS_NO = 0
SPI_CHIP_NO = 0
SPI_REQUEST = 22
SPI_READY = 27

# Configure the Python logging module to print to stderr. In your application,
# you may want to route the logging elsewhere.
logging.basicConfig()

# Import readline if the system has it
try:
    import readline
    assert readline # silence pyflakes
except ImportError:
    pass

# Import support for bitcoind RPC interface
import bitcoin
import bitcoin.rpc
from bitcoin.core import x, lx, b2x, b2lx, CMutableTxOut, CMutableTransaction
from bitcoin.wallet import CBitcoinAddress
bitcoin.SelectParams('mainnet')

TXTENNA_GATEWAY_GID = 2573394689

class goTennaCLI(cmd.Cmd):
    """ CLI handler function
    """
    def __init__(self):
        self.api_thread = None
        self.status = {}
        cmd.Cmd.__init__(self)
        self.prompt = 'txTenna>'
        self.in_flight_events = {}
        self._set_frequencies = False
        self._set_tx_power = False
        self._set_bandwidth = False
        self._set_geo_region = False
        self._settings = goTenna.settings.GoTennaSettings(
            rf_settings=goTenna.settings.RFSettings(), 
            geo_settings=goTenna.settings.GeoSettings())
        self._do_encryption = True
        self._awaiting_disconnect_after_fw_update = [False]
        self.messageIdx = 0
        self.local = False
        self.segment_storage = SegmentStorage()
        self.send_dir = None
        self.receive_dir = None
        self.watch_dir_thread = None
        self.pipe_file = None

    def precmd(self, line):
        if not self.api_thread\
           and not line.startswith('sdk_token')\
           and not line.startswith('quit'):
            print("An SDK token must be entered to begin.")
            return ''
        return line

    def do_sdk_token(self, rst):
        """ Enter an SDK token to begin usage of the driver. Usage: sdk_token TOKEN"""
        if self.api_thread:
            print("To change SDK tokens, restart the sample app.")
            return
        try:
            if not SPI_CONNECTION:
                self.api_thread = goTenna.driver.Driver(sdk_token=rst, gid=None, 
                                                    settings=None, 
                                                    event_callback=self.event_callback)
            else:
                self.api_thread = goTenna.driver.SpiDriver(
                                    SPI_BUS_NO, SPI_CHIP_NO, 22, 27,
                                    rst, None, None, self.event_callback)
            self.api_thread.start()
        except ValueError:
            print("SDK token {} is not valid. Please enter a valid SDK token."
                  .format(rst))

    def emptyline(self):
        pass

    def event_callback(self, evt):
        """ The event callback that will print messages from the API.

        See the documentation for ``goTenna.driver``.

        This will be invoked from the API's thread when events are received.
        """
        if evt.event_type == goTenna.driver.Event.MESSAGE:
            try:
                ## print(str(evt))
                self.handle_message(evt.message)
            except Exception:
                traceback.print_exc()
        elif evt.event_type == goTenna.driver.Event.DEVICE_PRESENT:
            ## print(str(evt))
            if self._awaiting_disconnect_after_fw_update[0]:
                print("Device physically connected")
            else:
                print("Device physically connected, configure to continue")
        elif evt.event_type == goTenna.driver.Event.CONNECT:
            if self._awaiting_disconnect_after_fw_update[0]:
                print("Device reconnected! Firmware update complete!")
                self._awaiting_disconnect_after_fw_update[0] = False
            else:
                print("Connected!")
                print(str(evt))
        elif evt.event_type == goTenna.driver.Event.DISCONNECT:
            if self._awaiting_disconnect_after_fw_update[0]:
                # Do not reset configuration so that the device will reconnect on its own
                print("Firmware update: Device disconnected, awaiting reconnect")
            else:
                print("Disconnected! {}".format(evt))
                # We reset the configuration here so that if the user plugs in a different
                # device it is not immediately reconfigured with new and incorrect data
                self.api_thread.set_gid(None)
                self.api_thread.set_rf_settings(None)
                self._set_frequencies = False
                self._set_tx_power = False
                self._set_bandwidth = False
        elif evt.event_type == goTenna.driver.Event.STATUS:
            self.status = evt.status
        elif evt.event_type == goTenna.driver.Event.GROUP_CREATE:
            index = -1
            for idx, member in enumerate(evt.group.members):
                if member.gid_val == self.api_thread.gid.gid_val:
                    index = idx
                    break
            print("Added to group {}: You are member {}"
                  .format(evt.group.gid.gid_val,
                          index))

    def build_callback(self, error_handler=None):
        """ Build a callback for sending to the API thread. May speciy a callable
        error_handler(details) taking the error details from the callback. The handler should return a string.
        """
        def default_error_handler(details):
            """ Easy error handler if no special behavior is needed. Just builds a string with the error.
            """
            if details['code'] in [goTenna.constants.ErrorCodes.TIMEOUT,
                                   goTenna.constants.ErrorCodes.OSERROR,
                                   goTenna.constants.ErrorCodes.EXCEPTION]:
                return "USB connection disrupted"
            return "Error: {}: {}".format(details['code'], details['msg'])
        # Define a second function here so it implicitly captures self
        captured_error_handler = [error_handler]
        def callback(correlation_id, success=None, results=None,
                     error=None, details=None):
            """ The default callback to pass to the API.

            See the documentation for ``goTenna.driver``.

            Does nothing but print whether the method succeeded or failed.
            """
            method = self.in_flight_events.pop(correlation_id.bytes,
                                               'Method call')
            if success:
                if results:
                    print("{} succeeded: {}".format(method, results))
                else:
                    print("{} succeeded!".format(method))
            elif error:
                if not captured_error_handler[0]:
                    captured_error_handler[0] = default_error_handler
                print("{} failed: {}".format(method,
                                             captured_error_handler[0](details)))
        return callback

    def do_set_gid(self, rem):
        """ Create a new profile (if it does not already exist) with default settings.

        Usage: set_gid GID

        GID should be a 15-digit numerical GID.
        """
        if self.api_thread.connected:
            print("Must not be connected when setting GID")
            return
        (gid, _) = self._parse_gid(rem, goTenna.settings.GID.PRIVATE)
        if not gid:
            return
        self.api_thread.set_gid(gid)

    def do_quit(self, arg):
        """ Safely quit.

        Usage: quit
        """
        # pylint: disable=unused-argument
        if self.api_thread:
            self.api_thread.join()
        sys.exit()

    def do_send_broadcast(self, message):
        """ Send a broadcast message

        Usage: send_broadcast MESSAGE
        """
        if not self.api_thread.connected:
            print("No device connected")
        else:
            def error_handler(details):
                """ A special error handler for formatting message failures
                """
                if details['code'] in [goTenna.constants.ErrorCodes.TIMEOUT,
                                       goTenna.constants.ErrorCodes.OSERROR]:
                    return "Message may not have been sent: USB connection disrupted"
                return "Error sending message: {}".format(details)
            try:
                method_callback = self.build_callback(error_handler)
                payload = goTenna.payload.TextPayload(message)
                print("payload valid = {}, message size = {}\n".format(payload.valid, len(message)))

                corr_id = self.api_thread.send_broadcast(payload, method_callback)
                while (corr_id is None):
                    ## try again if send_broadcast fails
                    sleep(10)
                    corr_id = self.api_thread.send_broadcast(payload, method_callback)

                self.in_flight_events[corr_id.bytes] = 'Broadcast message: {} ({} bytes)\n'.format(message,len(message))
            except ValueError:
                print("Message too long!")
                return

    @staticmethod
    def _parse_gid(line, gid_type, print_message=True):
        parts = line.split(' ')
        remainder = ' '.join(parts[1:])
        gidpart = parts[0]
        try:
            gid = int(gidpart)
            if gid > goTenna.constants.GID_MAX:
                print('{} is not a valid GID. The maximum GID is {}'
                      .format(str(gid), str(goTenna.constants.GID_MAX)))
                return (None, line)
            gidobj = goTenna.settings.GID(gid, gid_type)
            return (gidobj, remainder)
        except ValueError:
            if print_message:
                print('{} is not a valid GID.'.format(line))
            return (None, remainder)

    def do_send_private(self, rem):
        """ Send a private message to a contact

        Usage: send_private GID MESSAGE

        GID is the GID to send the private message to.

        MESSAGE is the message.
        """
        if not self.api_thread.connected:
            print("Must connect first")
            return
        (gid, rest) = self._parse_gid(rem, goTenna.settings.GID.PRIVATE)
        if not gid:
            return
        message = rest
        def error_handler(details):
            """ Special error handler for sending private messages to format errors
            """
            return "Error sending message: {}".format(details)

        try:
            method_callback = self.build_callback(error_handler)
            payload = goTenna.payload.TextPayload(message)
            def ack_callback(correlation_id, success):
                if success:
                    print("Private message to {}: delivery confirmed"
                          .format(gid.gid_val))
                else:
                    print("Private message to {}: delivery not confirmed, recipient may be offline or out of range"
                          .format(gid.gid_val))
            corr_id = self.api_thread.send_private(gid, payload,
                                                   method_callback,
                                                   ack_callback=ack_callback,
                                                   encrypt=self._do_encryption)
        except ValueError:
            print("Message too long!")
            return
        self.in_flight_events[corr_id.bytes]\
            = 'Private message to {}: {}'.format(gid.gid_val, message)

    def get_device_type(self):
        return self.api_thread.device_type

    def do_list_geo_region(self, rem):
        """ List the available region.

        Usage: list_geo_region
        """
        print("Allowed region:")
        for region in goTenna.constants.GEO_REGION.DICT:
            print("region {} : {}"
                  .format(region, goTenna.constants.GEO_REGION.DICT[region]))

    def do_set_geo_region(self, rem):
        """ Configure the frequencies the device will use.

        Usage: set_geo_region REGION

        Allowed region displayed with list_geo_region.
        """
        if self.get_device_type() == "pro":
            print("This configuration cannot be done for Pro devices.")
            return
        region = int(rem.strip())
        print('region={}'.format(region))
        if not goTenna.constants.GEO_REGION.valid(region):
            print("Invalid region setting {}".format(rem))
            return
        self._set_geo_region = True
        self._settings.geo_settings.region = region
        self.api_thread.set_geo_settings(self._settings.geo_settings)

    def do_can_connect(self, rem):
        """ Return whether a goTenna can connect. For a goTenna to connect, a GID and RF settings must be configured.
        """
        # pylint: disable=unused-argument
        if self.api_thread.gid:
            print("GID: OK")
        else:
            print("GID: Not Set")
        if self._set_tx_power:
            print("PRO - TX Power: OK")
        else:
            print("PRO - TX Power: Not Set")
        if self._set_frequencies:
            print("PRO - Frequencies: OK")
        else:
            print("PRO - Frequencies: Not Set")
        if self._set_bandwidth:
            print("PRO - Bandwidth: OK")
        else:
            print("PRO - Bandwidth: Not Set")
        if self._set_geo_region:
            print("MESH - Geo region: OK")
        else:
            print("MESH - Geo region: Not Set")

    def do_get_system_info(self, args):
        """ Get system information.

        Usage: get_system_info
        """
        if not self.api_thread.connected:
            print("Device must be connected")
        print(self.api_thread.system_info)

    def do_rpc_getrawtransaction(self, tx_id) :
        """
        Call local Bitcoin RPC method 'getrawtransaction'

        Usage: rpc_sendtoaddress TX_ID
        """
        try :
            proxy = bitcoin.rpc.Proxy()
            r = proxy.getrawtransaction(lx(tx_id), True)
            print(str(r))
        except:
            traceback.print_exc()

    def confirm_bitcoin_tx_local(self, hash, sender_gid):
        """ 
        Confirm bitcoin transaction using local bitcoind instance

        Usage: confirm_bitcoin_tx tx_id gid
        """ 

        ## send transaction to local bitcond
        segments = self.segment_storage.get_by_transaction_id(hash)
        raw_tx = self.segment_storage.get_raw_tx(segments)

        ## pass hex string converted to bytes
        try :
            proxy1 = bitcoin.rpc.Proxy()
            raw_tx_bytes = x(raw_tx)
            tx = CMutableTransaction.stream_deserialize(BytesIO(raw_tx_bytes))
            r1 = proxy1.sendrawtransaction(tx)
        except :
            print("Invalid Transaction! Could not send to network.")
            return

        ## try for 30 minutes to confirm the transaction
        for n in range(0, 30) :
            try :
                proxy2 = bitcoin.rpc.Proxy()
                r2 = proxy2.getrawtransaction(r1, True)

                ## send zero-conf message back to tx sender
                confirmations = r2.get('confirmations', 0)
                rObj = TxTennaSegment('', '', tx_hash=hash, block=confirmations)
                arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
                self.do_send_private(arg)

                print("\nSent to GID: " + str(sender_gid) + ": Transaction " + hash + " added to the mempool.")
                break      
            except IndexError:
                ## tx_id not yet in the global mempool, sleep for a minute and then try again
                sleep(60)
                continue      
            
            ## wait for atleast one confirmation
            for m in range(0, 30):
                sleep(60) # sleep for a minute
                try :
                    proxy3= bitcoin.rpc.Proxy()
                    r3 = proxy3.getrawtransaction(r1, True)
                    confirmations = r3.get('confirmations', 0)
                    ## keep waiting until 1 or more confirmations
                    if confirmations > 0:
                        break
                except :
                    ## unknown RPC error, but keep trying
                    traceback.print_exc()

            if confirmations > 0 :
                ## send confirmations message back to tx sender if confirmations > 0
                rObj = TxTennaSegment('', '', tx_hash=hash, block=confirmations)
                arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
                self.do_send_private(arg)
                print("\nSent to GID: " + str(sender_gid) + ", Transaction " + hash + " confirmed in " + str(confirmations) + " blocks.")
            else :
                print("\CTransaction from GID: " + str(sender_gid) + ", Transaction " + hash + " not confirmed after 30 minutes.")

    def confirm_bitcoin_tx_online(self, hash, sender_gid, network):
        """ confirm bitcoin transaction using default online Samourai API instance

        Usage: confirm_bitcoin_tx tx_id gid network
        """

        if  network == 't' :
            url = "https://api.samourai.io/test/v2/tx/" + hash ## default testnet txtenna-server
        else :
            url = "https://api.samourai.io/v2/tx/" + hash ## default txtenna-server
        
        try:
            r = requests.get(url)
            ## print(r.text)

            while r.status_code != 200:
                sleep(60) # sleep for a minute
                r = requests.get(url)

            ## send zero-conf message back to tx sender
            rObj = TxTennaSegment('', '', tx_hash=hash, block=0)
            arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
            self.do_send_private(arg)    

            print("\nSent to GID: " + str(sender_gid) + ": Transaction " + hash + " added to the mempool.")            

            r_text = "".join(r.text.split()) # remove whitespace
            obj = json.loads(r_text)
            while not 'block' in obj.keys():
                sleep(60) # sleep for a minute
                r = requests.get(url)
                r_text = "".join(r.text.split())
                obj = json.loads(r_text)

            ## send block height message back to tx sender
            blockheight = obj['block']['height']
            rObj = TxTennaSegment('', '', tx_hash=hash, block=blockheight)
            arg = str(sender_gid) + ' ' + rObj.serialize_to_json()
            self.do_send_private(arg)

            print("\nSent to GID: " + str(sender_gid) + ": Transaction " + hash + " confirmed in block " + str(blockheight) + ".")

        except:
            traceback.print_exc()

    def create_output_data_struct(self, data):
        """Create the output data structure generated by the blocksat receiver

        The "Protocol Sink" block of the blocksat-rx application places the incoming
        API data into output structures. This function creates the exact same
        structure that the blocksat-rx application would.

        Args:
            data : Sequence of bytes to be placed in the output structure

        Returns:
            Output data structure as sequence of bytes

        """

        # Header of the output data structure that the Blockstream Satellite Receiver
        # generates prior to writing user data into the API named pipe
        OUT_DATA_HEADER_FORMAT     = '64sQ'
        OUT_DATA_DELIMITER         = 'vyqzbefrsnzqahgdkrsidzigxvrppato' + \
                             '\xe0\xe0$\x1a\xe4["\xb5Z\x0bv\x17\xa7\xa7\x9d' + \
                             '\xa5\xd6\x00W}M\xa6TO\xda7\xfaeu:\xac\xdc'

        # Struct is composed of a delimiter and the message length
        out_data_header = struct.pack(OUT_DATA_HEADER_FORMAT,
                                    OUT_DATA_DELIMITER,
                                    len(data))

        # Final output data structure
        out_data = out_data_header + data

        return out_data            
            
    def receive_message_from_gateway(self, filename):
        """ 
        Receive message data from a mesh gateway node

        Usage: receive_message_from_gateway filename
        """ 

        ## send transaction to local blocksat reader pipe
        segments = self.segment_storage.get_by_transaction_id(filename)
        raw_data = self.segment_storage.get_raw_tx(segments).encode("utf-8")

        decoded_data = zlib.decompress(raw_data.decode('base64'))

        deliminted_data = self.create_output_data_struct(decoded_data)

        ## send the data to the blocksat pipe
        try :
            print("Message Data received for [" + filename + "] ( " + str(len(decoded_data)) + " bytes ) :\n" + str(decoded_data) + "\n")
        except UnicodeDecodeError :
            print("Binary Data received for [" + filename + "] ( " + str(len(decoded_data)) + " bytes )\n")
        
        if not self.pipe_file is None and os.path.exists(self.pipe_file) is True :
            # Open pipe and write raw data to it
            pipe_f = os.open(self.pipe_file, os.O_RDWR)
            os.write(pipe_f, deliminted_data)
        elif not self.receive_dir is None and os.path.exists(self.receive_dir) is True :
            # Create file
            dump_f = os.open(os.path.join(self.receive_dir, filename), os.O_CREAT | os.O_RDWR)
            os.write(dump_f, decoded_data)
        else :
            print("ERROR: Could not save data. No pipe found at [" + self.pipe_file + "] and no receive directory found at [" + self.receive_dir +"]\n")

    def handle_message(self, message):
        """ handle a txtenna message received over the mesh network

        Usage: handle_message message
        """
        payload = str(message.payload.message)
        print("received transaction payload: " + payload)

        segment = TxTennaSegment.deserialize_from_json(payload)
        self.segment_storage.put(segment)
        network = self.segment_storage.get_network(segment.payload_id)

        ## process incoming transaction confirmation from another server
        if (segment.block > 0):
            print("\nTransaction " + segment.payload_id + " confirmed in block " + str(segment.block))
        elif (segment.block is 0):
            print("\nTransaction " + segment.payload_id + " added to the the mem pool")
        elif (network is 'd'):
            ## process message data
            if (self.segment_storage.is_complete(segment.payload_id)):
                filename = self.segment_storage.get_transaction_id(segment.payload_id)
                t = Thread(target=self.receive_message_from_gateway, args=(filename,))
                t.start()
        else:
            ## process incoming tx segment
            if not self.local :
                headers = {u'content-type': u'application/json'}
                url = "https://api.samouraiwallet.com/v2/txtenna/segments" ## default txtenna-server
                r = requests.post(url, headers= headers, data=payload)
                print(r.text)

            if (self.segment_storage.is_complete(segment.payload_id)):
                sender_gid = message.sender.gid_val
                tx_id = self.segment_storage.get_transaction_id(segment.payload_id)

                ## check for confirmed transaction in a new thread
                if (self.local) :
                    t = Thread(target=self.confirm_bitcoin_tx_local, args=(tx_id, sender_gid))
                else :
                    t = Thread(target=self.confirm_bitcoin_tx_online, args=(tx_id, sender_gid, network))
                t.start()

    def do_mesh_broadcast_rawtx(self, rem):
        """ 
        Broadcast the raw hex of a Bitcoin transaction and its transaction ID over mainnet or testnet. 
        A local copy of txtenna-server must be configured to support the selected network.

        Usage: mesh_broadcast_tx RAW_HEX TX_ID NETWORK(m|t)

        eg. txTenna> mesh_broadcast_rawtx 01000000000101bf6c3ed233e8700b42c1369993c2078780015bab7067b9751b7f49f799efbffd0000000017160014f25dbf0eab0ba7e3482287ebb41a7f6d361de6efffffffff02204e00000000000017a91439cdb4242013e108337df383b1bf063561eb582687abb93b000000000017a9148b963056eedd4a02c91747ea667fc34548cab0848702483045022100e92ce9b5c91dbf1c976d10b2c5ed70d140318f3bf2123091d9071ada27a4a543022030c289d43298ca4ca9d52a4c85f95786c5e27de5881366d9154f6fe13a717f3701210204b40eff96588033722f487a52d39a345dc91413281b31909a4018efb330ba2600000000 94406beb94761fa728a2cde836ca636ecd3c51cbc0febc87a968cb8522ce7cc1 m
        """

        ## TODO: test Z85 binary encoding and add as an option
        (strHexTx, strHexTxHash, network) = rem.split(" ")
        gid = self.api_thread.gid.gid_val
        segments = TxTennaSegment.tx_to_segments(gid, strHexTx, strHexTxHash, str(self.messageIdx), network, False)
        for seg in segments :
            self.do_send_broadcast(seg.serialize_to_json())
            sleep(10)
        self.messageIdx = (self.messageIdx+1) % 9999

    def do_rpc_getbalance(self, rem) :
        """
        Call local Bitcoin RPC method 'getbalance'

        Usage: rpc_getbalance
        """
        try :
            proxy = bitcoin.rpc.Proxy()
            balance = proxy.getbalance()
            print("getbalance: " + str(balance))
        except Exception: # pylint: disable=broad-except
            traceback.print_exc()     

    def do_rpc_sendrawtransaction(self, hex) :
        """
        Call local Bitcoin RPC method 'sendrawtransaction'

        Usage: rpc_sendrawtransaction RAW_TX_HEX
        """
        try :
            proxy = bitcoin.rpc.Proxy()
            r = proxy.sendrawtransaction(hex)
            print("sendrawtransaction: " + str(r))
        except Exception: # pylint: disable=broad-except
            traceback.print_exc()

    def do_rpc_sendtoaddress(self, rem) :
        """
        Call local Bitcoin RPC method 'sendtoaddress'

        Usage: rpc_sendtoaddress ADDRESS SATS
        """
        try:
            proxy = bitcoin.rpc.Proxy()
            (addr, amount) = rem.split()
            r = proxy.sendtoaddress(addr, amount)
            print("sendtoaddress, transaction id: " + str(r["hex"]))
        except Exception: # pylint: disable=broad-except
            traceback.print_exc() 

    def do_mesh_sendtoaddress(self, rem) :
        """ 
        Create a signed transaction and broadcast it over the connected mesh device. The transaction 
        spends some amount of satoshis to the specified address from the local bitcoind wallet and selected network. 

        Usage: mesh_sendtoaddress ADDRESS SATS NETWORK(m|t)

        eg. txTenna> mesh_sendtoaddress 2N4BtwKZBU3kXkWT7ZBEcQLQ451AuDWiau2 13371337 t
        """
        try:

            proxy = bitcoin.rpc.Proxy()
            (addr, sats, network) = rem.split()

            # Create the txout. This time we create the scriptPubKey from a Bitcoin
            # address.
            txout = CMutableTxOut(sats, CBitcoinAddress(addr).to_scriptPubKey())

            # Create the unsigned transaction.
            unfunded_transaction = CMutableTransaction([], [txout])
            funded_transaction = proxy.fundrawtransaction(unfunded_transaction)
            signed_transaction = proxy.signrawtransaction(funded_transaction["tx"])
            txhex = b2x(signed_transaction["tx"].serialize())
            txid = b2lx(signed_transaction["tx"].GetTxid())
            print("sendtoaddress_mesh (tx, txid, network): " + txhex + ", " + txid, ", " + network)

            # broadcast over mesh
            self.do_mesh_broadcast_rawtx( txhex + " " + txid + " " + network)

        except Exception: # pylint: disable=broad-except
            traceback.print_exc()

        try :
            # lock UTXOs used to fund the tx if broadcast successful
            vin_outpoints = set()
            for txin in funded_transaction["tx"].vin:
                vin_outpoints.add(txin.prevout)
            ## json_outpoints = [{'txid':b2lx(outpoint.hash), 'vout':outpoint.n}
            ##              for outpoint in vin_outpoints]
            ## print(str(json_outpoints))
            proxy.lockunspent(False, vin_outpoints)
            
        except Exception: # pylint: disable=broad-except
            ## TODO: figure out why this is happening
            print("RPC timeout after calling lockunspent")

    def do_broadcast_messages(self, send_dir) :
        """ 
        Watch a particular directory for files with message data to be broadcast over the mesh network

        Usage: broadcast_messages DIRECTORY

        eg. txTenna> broadcast_messages ./downloads
        """

        if (send_dir is not None):
            #start new thread to watch directory
            self.watch_dir_thread = Thread(target=self.watch_messages, args=(send_dir,))
            self.watch_dir_thread.start()

    def watch_messages(self, send_dir):
        
        before = {}
        while os.path.exists(send_dir):
            sleep (10)
            after = dict ([(f, None) for f in os.listdir (send_dir)])
            new_files = [f for f in after if not f in before]
            if new_files:
                self.broadcast_message_files(send_dir, new_files)
            before = after

    def broadcast_message_files(self, directory, filenames):
        for filename in filenames:
            print("Broadcasting ",directory+"/"+filename)
            f = open(directory+"/"+filename,'r')
            message_data = f.read()
            f.close
            
            ## binary to ascii encoding and strip out newlines
            encoded = zlib.compress(message_data, 9).encode('base64').replace('\n','')
            print("[\n" + encoded.decode() + "\n]")

            gid = self.api_thread.gid.gid_val
            segments = TxTennaSegment.tx_to_segments(gid, encoded, filename, str(self.messageIdx), "d", False)
            for seg in segments :
                self.do_send_broadcast(seg.serialize_to_json())
                sleep(10)
            self.messageIdx = (self.messageIdx+1) % 9999


def run_cli():
    """
    The main function of the sample app.

    Instantiates a CLI object and runs it.
    """
    cli_obj = goTennaCLI()

    import argparse
    import six

    parser = argparse.ArgumentParser('Run a txTenna transaction gateway')
    parser.add_argument('SDK_TOKEN', type=six.b,
                        help='The token for the goTenna SDK')
    parser.add_argument('GEO_REGION', type=six.b,
                        help='The geo region number you are in')
    parser.add_argument("--gateway", action="store_true",
                        help="Use this computer as an internet connected transaction gateway with a default GID")
    parser.add_argument("--local", action="store_true",
                        help="Use local bitcoind to confirm and broadcast transactions")
    parser.add_argument("--send_dir",
                        help="Broadcast message data from files in this directory")
    parser.add_argument("--receive_dir",
                        help="Write files from received message data in this directory")
    parser.add_argument('-p', '--pipe',
                        default='/tmp/blocksat/api',
                        help='Pipe on which relayed message data is written out to ' +
                        '(default: /tmp/blocksat/api)')
    args = parser.parse_args()  

    ## start goTenna SDK thread by setting the SDK token
    cli_obj.do_sdk_token(args.SDK_TOKEN)

    ## set geo region
    cli_obj.do_set_geo_region(args.GEO_REGION)

    if args.gateway :
        ## use default gateway GID
        _gid = str(TXTENNA_GATEWAY_GID)
    else :
        ## create a random GID to use for sending transaction
        _gid = ''.join(random.SystemRandom().choice(string.hexdigits) for _ in range(12))
        _gid = str(int(_gid, 16))

    cli_obj.do_set_gid(_gid)
    print("set gid=",_gid)

    ## use local bitcoind to confirm transactions if 'local' is true
    cli_obj.local = args.local

    ## broadcast message data from files in this directory, eg. created by the blocksat
    cli_obj.send_dir = args.send_dir
    if (args.send_dir is not None):
        cli_obj.do_broadcast_messages(args.send_dir)

    cli_obj.pipe_file = args.pipe
    cli_obj.receive_dir = args.receive_dir

    try:
        sleep(5)
        cli_obj.cmdloop("Welcome to the txTenna API sample! "
                        "Press ? for a command list.\n")
    except Exception: # pylint: disable=broad-except
        traceback.print_exc()
        cli_obj.do_quit('')

if __name__ == '__main__':
    run_cli()

txtenna.py -  This code is derived from sample.py available from the goTenna Public SDK (https://github.com/gotenna/PublicSDK) # for the command line application # To quit # The goTenna API # For SPI connection only, set SPI_CONNECTION to true with proper SPI settings # Configure the Python logging module to print to stderr. In your application, # you may want to route the logging elsewhere. # Import readline if the system has it # silence pyflakes # Import support for bitcoind RPC interface CLI handler function Enter an SDK token to begin usage of the driver. Usage: sdk_token TOKEN The event callback that will print messages from the API.          See the documentation for ``goTenna.driver``.          This will be invoked from the API's thread when events are received. ## print(str(evt)) ## print(str(evt)) # Do not reset configuration so that the device will reconnect on its own # We reset the configuration here so that if the user plugs in a different # device it is not immediately reconfigured with new and incorrect data Build a callback for sending to the API thread. May speciy a callable         error_handler(details) taking the error details from the callback. The handler should return a string. Easy error handler if no special behavior is needed. Just builds a string with the error. # Define a second function here so it implicitly captures self The default callback to pass to the API.              See the documentation for ``goTenna.driver``.              Does nothing but print whether the method succeeded or failed. Create a new profile (if it does not already exist) with default settings.          Usage: set_gid GID          GID should be a 15-digit numerical GID. Safely quit.          Usage: quit # pylint: disable=unused-argument Send a broadcast message          Usage: send_broadcast MESSAGE A special error handler for formatting message failures ## try again if send_broadcast fails Send a private message to a contact          Usage: send_private GID MESSAGE          GID is the GID to send the private message to.          MESSAGE is the message. Special error handler for sending private messages to format errors List the available region.          Usage: list_geo_region Configure the frequencies the device will use.          Usage: set_geo_region REGION          Allowed region displayed with list_geo_region. Return whether a goTenna can connect. For a goTenna to connect, a GID and RF settings must be configured. # pylint: disable=unused-argument Get system information.          Usage: get_system_info Call local Bitcoin RPC method 'getrawtransaction'          Usage: rpc_sendtoaddress TX_ID Confirm bitcoin transaction using local bitcoind instance          Usage: confirm_bitcoin_tx tx_id gid ## send transaction to local bitcond ## pass hex string converted to bytes ## try for 30 minutes to confirm the transaction ## send zero-conf message back to tx sender ## tx_id not yet in the global mempool, sleep for a minute and then try again ## wait for atleast one confirmation # sleep for a minute ## keep waiting until 1 or more confirmations ## unknown RPC error, but keep trying ## send confirmations message back to tx sender if confirmations > 0 confirm bitcoin transaction using default online Samourai API instance          Usage: confirm_bitcoin_tx tx_id gid network ## default testnet txtenna-server ## default txtenna-server ## print(r.text) # sleep for a minute ## send zero-conf message back to tx sender # remove whitespace # sleep for a minute ## send block height message back to tx sender Create the output data structure generated by the blocksat receiver          The "Protocol Sink" block of the blocksat-rx application places the incoming         API data into output structures. This function creates the exact same         structure that the blocksat-rx application would.          Args:             data : Sequence of bytes to be placed in the output structure          Returns:             Output data structure as sequence of bytes # Header of the output data structure that the Blockstream Satellite Receiver # generates prior to writing user data into the API named pipe # Struct is composed of a delimiter and the message length # Final output data structure Receive message data from a mesh gateway node          Usage: receive_message_from_gateway filename ## send transaction to local blocksat reader pipe ## send the data to the blocksat pipe # Open pipe and write raw data to it # Create file handle a txtenna message received over the mesh network          Usage: handle_message message ## process incoming transaction confirmation from another server ## process message data ## process incoming tx segment ## default txtenna-server ## check for confirmed transaction in a new thread Broadcast the raw hex of a Bitcoin transaction and its transaction ID over mainnet or testnet.          A local copy of txtenna-server must be configured to support the selected network.          Usage: mesh_broadcast_tx RAW_HEX TX_ID NETWORK(m|t)          eg. txTenna> mesh_broadcast_rawtx 01000000000101bf6c3ed233e8700b42c1369993c2078780015bab7067b9751b7f49f799efbffd0000000017160014f25dbf0eab0ba7e3482287ebb41a7f6d361de6efffffffff02204e00000000000017a91439cdb4242013e108337df383b1bf063561eb582687abb93b000000000017a9148b963056eedd4a02c91747ea667fc34548cab0848702483045022100e92ce9b5c91dbf1c976d10b2c5ed70d140318f3bf2123091d9071ada27a4a543022030c289d43298ca4ca9d52a4c85f95786c5e27de5881366d9154f6fe13a717f3701210204b40eff96588033722f487a52d39a345dc91413281b31909a4018efb330ba2600000000 94406beb94761fa728a2cde836ca636ecd3c51cbc0febc87a968cb8522ce7cc1 m ## TODO: test Z85 binary encoding and add as an option Call local Bitcoin RPC method 'getbalance'          Usage: rpc_getbalance # pylint: disable=broad-except Call local Bitcoin RPC method 'sendrawtransaction'          Usage: rpc_sendrawtransaction RAW_TX_HEX # pylint: disable=broad-except Call local Bitcoin RPC method 'sendtoaddress'          Usage: rpc_sendtoaddress ADDRESS SATS # pylint: disable=broad-except Create a signed transaction and broadcast it over the connected mesh device. The transaction          spends some amount of satoshis to the specified address from the local bitcoind wallet and selected network.           Usage: mesh_sendtoaddress ADDRESS SATS NETWORK(m|t)          eg. txTenna> mesh_sendtoaddress 2N4BtwKZBU3kXkWT7ZBEcQLQ451AuDWiau2 13371337 t # Create the txout. This time we create the scriptPubKey from a Bitcoin # address. # Create the unsigned transaction. # broadcast over mesh # pylint: disable=broad-except # lock UTXOs used to fund the tx if broadcast successful ## json_outpoints = [{'txid':b2lx(outpoint.hash), 'vout':outpoint.n} ##              for outpoint in vin_outpoints] ## print(str(json_outpoints)) # pylint: disable=broad-except ## TODO: figure out why this is happening Watch a particular directory for files with message data to be broadcast over the mesh network          Usage: broadcast_messages DIRECTORY          eg. txTenna> broadcast_messages ./downloads #start new thread to watch directory ## binary to ascii encoding and strip out newlines The main function of the sample app.      Instantiates a CLI object and runs it. ## start goTenna SDK thread by setting the SDK token ## set geo region ## use default gateway GID ## create a random GID to use for sending transaction ## use local bitcoind to confirm transactions if 'local' is true ## broadcast message data from files in this directory, eg. created by the blocksat # pylint: disable=broad-except