From 785a4e35b913ebdbb7f3bdd1cba7913487684022 Mon Sep 17 00:00:00 2001
From: Scott Schurr <scott@ripple.com>
Date: Tue, 14 Aug 2018 14:45:58 -0700
Subject: [PATCH 1/6] Added AcctThread.py utility:

AcctThread is a command line utility that takes a ripple account
ID as an argument then walks the PreviousTxnID thread left
behind in the account root.  This walk will, if enough history
is available, pass through all transactions affecting the
account root in question until we arrive at the creation of the
account root.

Output is JSON (with text separators and text comments) to
stdout.  To capture the output to a file redirect the output.
---
 AcctThread.py | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 280 insertions(+)
 create mode 100755 AcctThread.py

diff --git a/AcctThread.py b/AcctThread.py
new file mode 100755
index 0000000..6359484
--- /dev/null
+++ b/AcctThread.py
@@ -0,0 +1,280 @@
+#!/usr/local/bin/python3
+
+#    Copyright (c) 2018 Ripple Labs Inc.
+#
+#    Permission to use, copy, modify, and/or distribute this software for any
+#    purpose  with  or without fee is hereby granted, provided that the above
+#    copyright notice and this permission notice appear in all copies.
+#
+#    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+#    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
+#    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+#    ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+#    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
+#    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+#    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
+# Walk back through an account node's history and report all transactions
+# that affect the account in reverse order (from newest to oldest).
+#
+# To capture results redirect stdout to a file.
+
+import json
+import re
+import sys
+import websocket
+
+from websocket import create_connection
+
+# Extract command line arguments.
+def extractArgs ():
+
+    # Default websocket connection if none provided.
+    connectTo = "ws://s2.ripple.com:443"
+
+    usage = 'usage: PyAcctThread <Account ID> [ws://<Server>:<port>]\n'\
+        'If <server>:<port> are omitted defaults to "{0}"'.format (connectTo)
+
+    # Extract the first argument: the accountID
+    argCount = len (sys.argv) - 1
+    if argCount < 1:
+        print ('Expected account ID as first argument.\n')
+        print (usage)
+        sys.exit (1)  # abort because of error
+
+    # Sanity check the accountID string
+    accountId = sys.argv[1]
+    idLen = len (accountId)
+    if (accountId[:1] != "r") or (idLen < 25) or (idLen > 35):
+        print ('Invalid format for account ID.\n'\
+            'Should start with "r" with length between 25 and 35 characters.\n')
+        print (usage)
+        sys.exit (1)  # abort because of error
+
+    if argCount > 2:
+        print ('Too many command line arguments.\n')
+        print (usage)
+        sys.exit (1)  # abort because of error
+
+    # If it's there, pick up the optional connectTo.
+    if argCount == 2:
+        connectTo = sys.argv[2]
+
+    # Validate the connectTo.
+    if connectTo[:5] != "ws://":
+        print ('Invalid format for websocket connection.  Expected "ws://".  '\
+            'Got: {0}\n'.format (connectTo))
+        print (usage)
+        sys.exit (1)  # abort because of error
+
+    # Verify that the port is specified.
+    if not re.search (r'\:\d+$', connectTo):
+        print ('Invalid format for websocket connection.  Connection expected '\
+            'to end with \nport specifier (colon followed by digits), '\
+            'e.g., ws://s2.ripple.com:443')
+        print ('Got: {0}\n'.format (connectTo))
+        print (usage)
+        sys.exit (1)  # abort because of error
+
+    return connectTo, accountId
+
+
+# Used to track websocket requests.
+wsIdValue = 0
+
+# Generate a new websocket request ID and return it.
+def wsId ():
+    global wsIdValue
+    wsIdValue += 1
+    return wsIdValue
+
+
+# Get the websocket response that matches the id of the request.  All
+# responses are in json, so return json.
+def getResponse (ws, id):
+    while True:
+        msg = ws.recv ()
+        jsonMsg = json.loads (msg)
+        gotId = jsonMsg["id"]
+        if (gotId == id):
+            return jsonMsg
+        print ("Unexpected websocket message id: {0}.  Expected {1}.".format (
+            gotId, id))
+
+
+# Request account_info, print it, and return it as JSON.
+def printAccountInfo (ws, accountId):
+    id = wsId ()
+
+    cmd = """
+{{
+    "id" : {0},
+    "command" : "account_info",
+    "account" : "{1}",
+    "strict" : true,
+    "ledger_index" : "validated"
+}}""".format (id, accountId)
+
+    ws.send (cmd)
+    jsonMsg = getResponse (ws, id)
+
+    # Remove the websocket id to reduce noise.
+    jsonMsg.pop ("id", None)
+    print (json.dumps (jsonMsg, indent = 4))
+    return jsonMsg
+
+
+# Request tx by txnId, print it, and return the tx as JSON.
+def printTx (ws, txnId):
+    id = wsId ()
+
+    cmd = """
+{{
+    "id" : {0},
+    "command" : "tx",
+    "transaction" : "{1}"
+}}""".format (id, txnId)
+
+    ws.send (cmd)
+    jsonMsg = getResponse (ws, id)
+
+    # Remove the websocket id from what we print to reduce noise.
+    jsonMsg.pop ("id", None)
+    print (json.dumps (jsonMsg, indent = 4))
+    return jsonMsg
+
+
+# Extract the PreviousTxnID for accountID given a transaction's metadata.
+def getPrevTxnId (jsonMsg, accountId):
+
+    # Handle any errors that might be in the response.
+    if "error" in jsonMsg:
+
+        # If the error is txnNotFound there there's a good chance
+        # the server does not have enough history.
+        if jsonMsg["error"] == "txnNotFound":
+            print ("Transaction not found.  "
+                "Does your server have enough history?  Unexpected stop.")
+            return ""
+
+        err = jsonMsg["error"]
+        if jsonMsg[error_message]:
+            err = jsonMsg[error_message]
+
+        print (err + "  Unexpected stop.")
+        return ""
+
+    # First navigate to the metadata AffectedNodes, which should be a list.
+    try:
+        affectedNodes = jsonMsg["result"]["meta"]["AffectedNodes"]
+    except KeyError:
+        print ("No AffectedNodes found in transaction.  Unexpected stop.\n")
+        print ("Got:")
+        print (json.dumps (jsonMsg, indent = 4))
+        return ""
+
+    for node in affectedNodes:
+        # If we find the account being created then we're successfully done.
+        try:
+            if node["CreatedNode"]["LedgerEntryType"] == "AccountRoot":
+                if node["CreatedNode"]["NewFields"]["Account"] == accountId:
+                    print (
+                        'Created Account {0}.  Done.'.format (accountId))
+                    return ""
+        except KeyError:
+            pass # If the field is not found that's okay.
+
+        # Else look for the next transaction.
+        try:
+            if node["ModifiedNode"]["FinalFields"]["Account"] == accountId:
+                return node["ModifiedNode"]["PreviousTxnID"]
+        except KeyError:
+            continue;  # If the field is not found try the next node.
+
+    print ("No more modifying transactions found.  Unexpected stop.\n")
+    print ("Got:")
+    print (json.dumps (jsonMsg, indent = 4))
+    return ""
+
+
+# Write spinner to stderr to show liveness while walking the thread.
+def SpinningCursor ():
+    while True:
+        for cursor in '|/-\\':
+            sys.stderr.write (cursor)
+            sys.stderr.flush ()
+            yield
+            sys.stderr.write ('\b')
+
+
+# Thread the account to extract all transactions performed by that account.
+#    1. Start by getting account_info for the accountId in the most recent
+#       validated ledger.
+#    2. account_info returns the TxId of the last transaction that affected
+#       this account.
+#    3. Call tx with that TxId.  Save that transaction.
+#    4. The tx response should contain the AccountRoot for the accountId.
+#       Extract the new value of PreviousTxID from the AccountRoot.
+#    5. Return to step 3, but using the new PreviousTxID.
+def threadAccount (ws, accountId):
+
+    # Call account_info to get our starting txId.
+    jsonMsg = printAccountInfo (ws, accountId)
+
+    # Handle any errors that might be in the response.
+    if "error" in jsonMsg:
+        print ('No account_info for accountID {0}'.format (accountId))
+        if jsonMsg["error"] == "actMalformed":
+            print ("Did you mistype the accountID?")
+
+        err = jsonMsg["error"]
+        if jsonMsg["error_message"]:
+            err = jsonMsg["error_message"]
+
+        print (err + "  Unexpected stop.")
+        return
+
+    # Extract the starting txId.
+    prevTxnId = ""
+    try:
+        prevTxnId = jsonMsg["result"]["account_data"]["PreviousTxnID"]
+    except KeyError:
+        print (
+            "No PreviousTxnID found for {0}.  No transactions found.".format (
+                accountId))
+        return
+
+    # Transaction threading loop.
+    spinner = SpinningCursor ()  # Liveness indicator.
+    while prevTxnId != "":
+        next (spinner)
+        print ("\n" + ("-" * 79) + "\n")
+        jsonMsg = printTx (ws, prevTxnId)
+        prevTxnId = getPrevTxnId (jsonMsg, accountId)
+
+
+# Open the websocket connection and pass the websocket upstream for use.
+def openConnection (connectTo, accountId):
+    try:
+        ws = create_connection (connectTo)
+    except websocket._exceptions.WebSocketAddressException:
+        print ("Unable to open connection to {0}.\n".format (connectTo))
+        return;
+    except ConnectionRefusedError:
+        print ("Connection to {0} refused.\n".format (connectTo))
+        return
+
+    try:
+        threadAccount (ws, accountId)
+    finally:
+        ws.close ()
+        sys.stderr.write ('\b') # Clean up from SpinningCursor
+
+
+if __name__ == "__main__":
+    # Get command line arguments.
+    connectTo, accountId = extractArgs ()
+
+    # Open the connection then thread the account.
+    openConnection (connectTo, accountId)

From 45a7229a31305b0457710b15ce975fa4c2d881c2 Mon Sep 17 00:00:00 2001
From: seelabs <scott.determan@yahoo.com>
Date: Wed, 15 Aug 2018 08:49:27 -0400
Subject: [PATCH 2/6] [fold] Feedback from review

---
 AcctThread.py | 310 +++++++++++++++++++++++++-------------------------
 1 file changed, 153 insertions(+), 157 deletions(-)

diff --git a/AcctThread.py b/AcctThread.py
index 6359484..acbbe48 100755
--- a/AcctThread.py
+++ b/AcctThread.py
@@ -1,4 +1,4 @@
-#!/usr/local/bin/python3
+#!/usr/bin/env python3
 
 #    Copyright (c) 2018 Ripple Labs Inc.
 #
@@ -14,7 +14,6 @@
 #    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 #    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
-
 # Walk back through an account node's history and report all transactions
 # that affect the account in reverse order (from newest to oldest).
 #
@@ -27,254 +26,251 @@
 
 from websocket import create_connection
 
-# Extract command line arguments.
-def extractArgs ():
+
+def extract_args():
+    '''Extract command line arguments'''
 
     # Default websocket connection if none provided.
-    connectTo = "ws://s2.ripple.com:443"
+    connect_to = "ws://s2.ripple.com:443"
 
-    usage = 'usage: PyAcctThread <Account ID> [ws://<Server>:<port>]\n'\
-        'If <server>:<port> are omitted defaults to "{0}"'.format (connectTo)
+    usage = f'''usage: PyAcctThread <Account ID> [ws://<Server>:<port>]
+If <server>:<port> are omitted defaults to "{connect_to}"'''
 
     # Extract the first argument: the accountID
-    argCount = len (sys.argv) - 1
-    if argCount < 1:
-        print ('Expected account ID as first argument.\n')
-        print (usage)
-        sys.exit (1)  # abort because of error
+    arg_count = len(sys.argv) - 1
+    if arg_count < 1:
+        print('Expected account ID as first argument.\n')
+        print(usage)
+        sys.exit(1)  # abort because of error
 
     # Sanity check the accountID string
-    accountId = sys.argv[1]
-    idLen = len (accountId)
-    if (accountId[:1] != "r") or (idLen < 25) or (idLen > 35):
-        print ('Invalid format for account ID.\n'\
-            'Should start with "r" with length between 25 and 35 characters.\n')
-        print (usage)
-        sys.exit (1)  # abort because of error
-
-    if argCount > 2:
-        print ('Too many command line arguments.\n')
-        print (usage)
-        sys.exit (1)  # abort because of error
-
-    # If it's there, pick up the optional connectTo.
-    if argCount == 2:
-        connectTo = sys.argv[2]
-
-    # Validate the connectTo.
-    if connectTo[:5] != "ws://":
-        print ('Invalid format for websocket connection.  Expected "ws://".  '\
-            'Got: {0}\n'.format (connectTo))
-        print (usage)
-        sys.exit (1)  # abort because of error
+    account_id = sys.argv[1]
+    id_len = len(account_id)
+    if (account_id[:1] != "r") or (id_len < 25) or (id_len > 35):
+        print(
+            'Invalid format for account ID.\n',
+            'Should start with "r" with length between 25 and 35 characters.\n'
+        )
+        print(usage)
+        sys.exit(1)  # abort because of error
+
+    if arg_count > 2:
+        print('Too many command line arguments.\n')
+        print(usage)
+        sys.exit(1)  # abort because of error
+
+    # If it's there, pick up the optional connect_to.
+    if arg_count == 2:
+        connect_to = sys.argv[2]
+
+    # Validate the connect_to.
+    if connect_to[:5] != "ws://":
+        print('Invalid format for websocket connection.  Expected "ws://".  ',
+              f'Got: {connect_to}\n')
+        print(usage)
+        sys.exit(1)  # abort because of error
 
     # Verify that the port is specified.
-    if not re.search (r'\:\d+$', connectTo):
-        print ('Invalid format for websocket connection.  Connection expected '\
-            'to end with \nport specifier (colon followed by digits), '\
-            'e.g., ws://s2.ripple.com:443')
-        print ('Got: {0}\n'.format (connectTo))
-        print (usage)
-        sys.exit (1)  # abort because of error
-
-    return connectTo, accountId
+    if not re.search(r'\:\d+$', connect_to):
+        print('Invalid format for websocket connection.  Connection expected ',
+              'to end with \nport specifier (colon followed by digits), ',
+              'e.g., ws://s2.ripple.com:443')
+        print(f'Got: {connect_to}\n')
+        print(usage)
+        sys.exit(1)  # abort because of error
 
+    return connect_to, account_id
 
-# Used to track websocket requests.
-wsIdValue = 0
 
-# Generate a new websocket request ID and return it.
-def wsId ():
-    global wsIdValue
-    wsIdValue += 1
-    return wsIdValue
+def ws_id():
+    '''Generate a new websocket request ID and return it'''
+    ws_id.value += 1
+    return ws_id.value
+ws_id.value = 0
 
 
-# Get the websocket response that matches the id of the request.  All
-# responses are in json, so return json.
-def getResponse (ws, id):
+def get_response(ws, req_id):
+    '''Get the websocket response that matches the id of the request.
+       All responses are in json, so return json.'''
     while True:
-        msg = ws.recv ()
-        jsonMsg = json.loads (msg)
-        gotId = jsonMsg["id"]
-        if (gotId == id):
-            return jsonMsg
-        print ("Unexpected websocket message id: {0}.  Expected {1}.".format (
-            gotId, id))
+        msg = ws.recv()
+        json_msg = json.loads(msg)
+        got_id = json_msg["id"]
+        if got_id == req_id:
+            return json_msg
+        print(
+            f"Unexpected websocket message id: {got_id}.  Expected {req_id}.")
 
 
-# Request account_info, print it, and return it as JSON.
-def printAccountInfo (ws, accountId):
-    id = wsId ()
+def print_account_info(ws, account_id):
+    '''Request account_info, print it, and return it as JSON'''
+    wsid = ws_id()
 
-    cmd = """
+    cmd = f"""
 {{
-    "id" : {0},
+    "id" : {wsid},
     "command" : "account_info",
-    "account" : "{1}",
+    "account" : "{account_id}",
     "strict" : true,
     "ledger_index" : "validated"
-}}""".format (id, accountId)
+}}"""
 
-    ws.send (cmd)
-    jsonMsg = getResponse (ws, id)
+    ws.send(cmd)
+    json_msg = get_response(ws, wsid)
 
     # Remove the websocket id to reduce noise.
-    jsonMsg.pop ("id", None)
-    print (json.dumps (jsonMsg, indent = 4))
-    return jsonMsg
+    json_msg.pop("id", None)
+    print(json.dumps(json_msg, indent=4))
+    return json_msg
 
 
-# Request tx by txnId, print it, and return the tx as JSON.
-def printTx (ws, txnId):
-    id = wsId ()
+def print_tx(ws, txn_id):
+    '''Request tx by tnx_id, print it, and return the tx as JSON'''
+    wsid = ws_id()
 
-    cmd = """
+    cmd = f"""
 {{
-    "id" : {0},
+    "id" : {wsid},
     "command" : "tx",
-    "transaction" : "{1}"
-}}""".format (id, txnId)
+    "transaction" : "{txn_id}"
+}}"""
 
-    ws.send (cmd)
-    jsonMsg = getResponse (ws, id)
+    ws.send(cmd)
+    json_msg = get_response(ws, wsid)
 
     # Remove the websocket id from what we print to reduce noise.
-    jsonMsg.pop ("id", None)
-    print (json.dumps (jsonMsg, indent = 4))
-    return jsonMsg
+    json_msg.pop("id", None)
+    print(json.dumps(json_msg, indent=4))
+    return json_msg
 
 
-# Extract the PreviousTxnID for accountID given a transaction's metadata.
-def getPrevTxnId (jsonMsg, accountId):
+def get_prev_txn_id(json_msg, account_id):
+    '''Extract the PreviousTxnID for accountID given a transaction's metadata'''
 
     # Handle any errors that might be in the response.
-    if "error" in jsonMsg:
+    if "error" in json_msg:
 
         # If the error is txnNotFound there there's a good chance
         # the server does not have enough history.
-        if jsonMsg["error"] == "txnNotFound":
-            print ("Transaction not found.  "
-                "Does your server have enough history?  Unexpected stop.")
+        if json_msg["error"] == "txnNotFound":
+            print("Transaction not found.",
+                  "Does your server have enough history? Unexpected stop.")
             return ""
 
-        err = jsonMsg["error"]
-        if jsonMsg[error_message]:
-            err = jsonMsg[error_message]
+        err = json_msg["error"]
+        if json_msg['error_message']:
+            err = json_msg['error_message']
 
-        print (err + "  Unexpected stop.")
+        print(f"{err}  Unexpected stop.")
         return ""
 
     # First navigate to the metadata AffectedNodes, which should be a list.
     try:
-        affectedNodes = jsonMsg["result"]["meta"]["AffectedNodes"]
+        affected_nodes = json_msg["result"]["meta"]["AffectedNodes"]
     except KeyError:
-        print ("No AffectedNodes found in transaction.  Unexpected stop.\n")
-        print ("Got:")
-        print (json.dumps (jsonMsg, indent = 4))
+        print("No AffectedNodes found in transaction.  Unexpected stop.\n")
+        print("Got:")
+        print(json.dumps(json_msg, indent=4))
         return ""
 
-    for node in affectedNodes:
+    for node in affected_nodes:
         # If we find the account being created then we're successfully done.
         try:
             if node["CreatedNode"]["LedgerEntryType"] == "AccountRoot":
-                if node["CreatedNode"]["NewFields"]["Account"] == accountId:
-                    print (
-                        'Created Account {0}.  Done.'.format (accountId))
+                if node["CreatedNode"]["NewFields"]["Account"] == account_id:
+                    print(f'Created Account {account_id}.  Done.')
                     return ""
         except KeyError:
-            pass # If the field is not found that's okay.
+            pass  # If the field is not found that's okay.
 
         # Else look for the next transaction.
         try:
-            if node["ModifiedNode"]["FinalFields"]["Account"] == accountId:
+            if node["ModifiedNode"]["FinalFields"]["Account"] == account_id:
                 return node["ModifiedNode"]["PreviousTxnID"]
         except KeyError:
-            continue;  # If the field is not found try the next node.
+            continue
+            # If the field is not found try the next node.
 
-    print ("No more modifying transactions found.  Unexpected stop.\n")
-    print ("Got:")
-    print (json.dumps (jsonMsg, indent = 4))
+    print("No more modifying transactions found.  Unexpected stop.\n")
+    print("Got:")
+    print(json.dumps(json_msg, indent=4))
     return ""
 
 
-# Write spinner to stderr to show liveness while walking the thread.
-def SpinningCursor ():
+def spinning_cursor():
+    '''Write spinner to stderr to show liveness while walking the thread'''
     while True:
         for cursor in '|/-\\':
-            sys.stderr.write (cursor)
-            sys.stderr.flush ()
+            sys.stderr.write(cursor)
+            sys.stderr.flush()
             yield
-            sys.stderr.write ('\b')
+            sys.stderr.write('\b')
 
 
-# Thread the account to extract all transactions performed by that account.
-#    1. Start by getting account_info for the accountId in the most recent
-#       validated ledger.
-#    2. account_info returns the TxId of the last transaction that affected
-#       this account.
-#    3. Call tx with that TxId.  Save that transaction.
-#    4. The tx response should contain the AccountRoot for the accountId.
-#       Extract the new value of PreviousTxID from the AccountRoot.
-#    5. Return to step 3, but using the new PreviousTxID.
-def threadAccount (ws, accountId):
+def thread_account(ws, account_id):
+    '''Thread the account to extract all transactions performed by that account.
+       1. Start by getting account_info for the account_id in the most recent
+          validated ledger.
+       2. account_info returns the TxId of the last transaction that affected
+          this account.
+       3. Call tx with that TxId.  Save that transaction.
+       4. The tx response should contain the AccountRoot for the account_id.
+          Extract the new value of PreviousTxID from the AccountRoot.
+       5. Return to step 3, but using the new PreviousTxID.'''
 
     # Call account_info to get our starting txId.
-    jsonMsg = printAccountInfo (ws, accountId)
+    json_msg = print_account_info(ws, account_id)
 
     # Handle any errors that might be in the response.
-    if "error" in jsonMsg:
-        print ('No account_info for accountID {0}'.format (accountId))
-        if jsonMsg["error"] == "actMalformed":
-            print ("Did you mistype the accountID?")
+    if "error" in json_msg:
+        print(f'No account_info for accountID {account_id}')
+        if json_msg["error"] == "actMalformed":
+            print("Did you mistype the accountID?")
 
-        err = jsonMsg["error"]
-        if jsonMsg["error_message"]:
-            err = jsonMsg["error_message"]
+        err = json_msg["error"]
+        if json_msg["error_message"]:
+            err = json_msg["error_message"]
 
-        print (err + "  Unexpected stop.")
+        print(f"{err}  Unexpected stop.")
         return
 
     # Extract the starting txId.
-    prevTxnId = ""
+    prev_txn_id = ""
     try:
-        prevTxnId = jsonMsg["result"]["account_data"]["PreviousTxnID"]
+        prev_txn_id = json_msg["result"]["account_data"]["PreviousTxnID"]
     except KeyError:
-        print (
-            "No PreviousTxnID found for {0}.  No transactions found.".format (
-                accountId))
+        print(
+            f"No PreviousTxnID found for {account_id}.  No transactions found."
+        )
         return
 
     # Transaction threading loop.
-    spinner = SpinningCursor ()  # Liveness indicator.
-    while prevTxnId != "":
-        next (spinner)
-        print ("\n" + ("-" * 79) + "\n")
-        jsonMsg = printTx (ws, prevTxnId)
-        prevTxnId = getPrevTxnId (jsonMsg, accountId)
+    spinner = spinning_cursor()  # Liveness indicator.
+    while prev_txn_id != "":
+        next(spinner)
+        print("\n" + ("-" * 79) + "\n")
+        json_msg = print_tx(ws, prev_txn_id)
+        prev_txn_id = get_prev_txn_id(json_msg, account_id)
 
 
-# Open the websocket connection and pass the websocket upstream for use.
-def openConnection (connectTo, accountId):
+def open_connection(connect_to, account_id):
+    '''Open the websocket connection and pass the websocket upstream for use'''
     try:
-        ws = create_connection (connectTo)
+        ws = create_connection(connect_to)
     except websocket._exceptions.WebSocketAddressException:
-        print ("Unable to open connection to {0}.\n".format (connectTo))
-        return;
+        print(f"Unable to open connection to {connect_to}.\n")
+        return
     except ConnectionRefusedError:
-        print ("Connection to {0} refused.\n".format (connectTo))
+        print(f"Connection to {connect_to} refused.\n")
         return
 
     try:
-        threadAccount (ws, accountId)
+        thread_account(ws, account_id)
     finally:
-        ws.close ()
-        sys.stderr.write ('\b') # Clean up from SpinningCursor
+        ws.close()
+        sys.stderr.write('\b')  # Clean up from spinning_cursor
 
 
 if __name__ == "__main__":
-    # Get command line arguments.
-    connectTo, accountId = extractArgs ()
-
     # Open the connection then thread the account.
-    openConnection (connectTo, accountId)
+    open_connection(*extract_args())

From 8ba1d4511adbe8d4b0ca69028bda2c463b1e6d37 Mon Sep 17 00:00:00 2001
From: Scott Schurr <scott@ripple.com>
Date: Wed, 15 Aug 2018 11:49:09 -0700
Subject: [PATCH 3/6] [FOLD] Commands are dictionaries, improve error responses

---
 AcctThread.py | 47 ++++++++++++++++++++---------------------------
 1 file changed, 20 insertions(+), 27 deletions(-)

diff --git a/AcctThread.py b/AcctThread.py
index acbbe48..6e34c7d 100755
--- a/AcctThread.py
+++ b/AcctThread.py
@@ -33,7 +33,7 @@ def extract_args():
     # Default websocket connection if none provided.
     connect_to = "ws://s2.ripple.com:443"
 
-    usage = f'''usage: PyAcctThread <Account ID> [ws://<Server>:<port>]
+    usage = f'''usage: PyAcctThread <Account ID> [ws://<server>:<port>]
 If <server>:<port> are omitted defaults to "{connect_to}"'''
 
     # Extract the first argument: the accountID
@@ -48,8 +48,8 @@ def extract_args():
     id_len = len(account_id)
     if (account_id[:1] != "r") or (id_len < 25) or (id_len > 35):
         print(
-            'Invalid format for account ID.\n',
-            'Should start with "r" with length between 25 and 35 characters.\n'
+            'Invalid format for account ID. Should start with "r" and',
+            '\nlength should be between 25 and 35 characters.\n'
         )
         print(usage)
         sys.exit(1)  # abort because of error
@@ -65,8 +65,8 @@ def extract_args():
 
     # Validate the connect_to.
     if connect_to[:5] != "ws://":
-        print('Invalid format for websocket connection.  Expected "ws://".  ',
-              f'Got: {connect_to}\n')
+        print('Invalid format for websocket connection.',
+              f'\nExpected: ws://...  Got: {connect_to}\n')
         print(usage)
         sys.exit(1)  # abort because of error
 
@@ -106,16 +106,15 @@ def print_account_info(ws, account_id):
     '''Request account_info, print it, and return it as JSON'''
     wsid = ws_id()
 
-    cmd = f"""
-{{
-    "id" : {wsid},
-    "command" : "account_info",
-    "account" : "{account_id}",
-    "strict" : true,
-    "ledger_index" : "validated"
-}}"""
+    cmd = {
+        "id" : wsid,
+        "command" : "account_info",
+        "account" : account_id,
+        "strict" : True,
+        "ledger_index" : "validated"
+    }
 
-    ws.send(cmd)
+    ws.send(json.dumps(cmd))
     json_msg = get_response(ws, wsid)
 
     # Remove the websocket id to reduce noise.
@@ -128,14 +127,13 @@ def print_tx(ws, txn_id):
     '''Request tx by tnx_id, print it, and return the tx as JSON'''
     wsid = ws_id()
 
-    cmd = f"""
-{{
-    "id" : {wsid},
-    "command" : "tx",
-    "transaction" : "{txn_id}"
-}}"""
+    cmd = {
+        "id": wsid,
+        "command": "tx",
+        "transaction": txn_id
+    }
 
-    ws.send(cmd)
+    ws.send(json.dumps(cmd))
     json_msg = get_response(ws, wsid)
 
     # Remove the websocket id from what we print to reduce noise.
@@ -169,8 +167,6 @@ def get_prev_txn_id(json_msg, account_id):
         affected_nodes = json_msg["result"]["meta"]["AffectedNodes"]
     except KeyError:
         print("No AffectedNodes found in transaction.  Unexpected stop.\n")
-        print("Got:")
-        print(json.dumps(json_msg, indent=4))
         return ""
 
     for node in affected_nodes:
@@ -188,12 +184,9 @@ def get_prev_txn_id(json_msg, account_id):
             if node["ModifiedNode"]["FinalFields"]["Account"] == account_id:
                 return node["ModifiedNode"]["PreviousTxnID"]
         except KeyError:
-            continue
-            # If the field is not found try the next node.
+            continue  # If the field is not found try the next node.
 
     print("No more modifying transactions found.  Unexpected stop.\n")
-    print("Got:")
-    print(json.dumps(json_msg, indent=4))
     return ""
 
 

From 533261a9bd8e2cf4600f7893a79fb3ef36d501f1 Mon Sep 17 00:00:00 2001
From: Scott Schurr <scott@ripple.com>
Date: Wed, 15 Aug 2018 12:07:17 -0700
Subject: [PATCH 4/6] [FOLD] Move AcctThread.py to a subdirectory

---
 AcctThread.py => acctThread/AcctThread.py | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename AcctThread.py => acctThread/AcctThread.py (100%)

diff --git a/AcctThread.py b/acctThread/AcctThread.py
similarity index 100%
rename from AcctThread.py
rename to acctThread/AcctThread.py

From ae5ad584ad64f49136ceb53d89f0b8b2bdee9416 Mon Sep 17 00:00:00 2001
From: Scott Schurr <scott@ripple.com>
Date: Wed, 15 Aug 2018 12:42:14 -0700
Subject: [PATCH 5/6] [FOLD] Create scripts subdirectory

---
 {acctThread => scripts}/AcctThread.py | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename {acctThread => scripts}/AcctThread.py (100%)

diff --git a/acctThread/AcctThread.py b/scripts/AcctThread.py
similarity index 100%
rename from acctThread/AcctThread.py
rename to scripts/AcctThread.py

From 8e39f131f1bfc180fd54419beb899f5498879ebe Mon Sep 17 00:00:00 2001
From: Scott Schurr <scott@ripple.com>
Date: Thu, 16 Aug 2018 17:40:15 -0700
Subject: [PATCH 6/6] [FOLD] More improvements from review

---
 scripts/AcctThread.py | 54 +++++++++++++++++++++----------------------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/scripts/AcctThread.py b/scripts/AcctThread.py
index 6e34c7d..6691cb6 100755
--- a/scripts/AcctThread.py
+++ b/scripts/AcctThread.py
@@ -18,11 +18,13 @@
 # that affect the account in reverse order (from newest to oldest).
 #
 # To capture results redirect stdout to a file.
+#
+# NOTE: Requires installation of non-standard websocket-client module.
 
 import json
 import re
 import sys
-import websocket
+import websocket  # From websocket-client module.
 
 from websocket import create_connection
 
@@ -31,9 +33,9 @@ def extract_args():
     '''Extract command line arguments'''
 
     # Default websocket connection if none provided.
-    connect_to = "ws://s2.ripple.com:443"
+    connect_to = "wss://s2.ripple.com:443"
 
-    usage = f'''usage: PyAcctThread <Account ID> [ws://<server>:<port>]
+    usage = f'''usage: PyAcctThread <Account ID> [wss://<server>:<port>]
 If <server>:<port> are omitted defaults to "{connect_to}"'''
 
     # Extract the first argument: the accountID
@@ -64,17 +66,17 @@ def extract_args():
         connect_to = sys.argv[2]
 
     # Validate the connect_to.
-    if connect_to[:5] != "ws://":
+    if not re.search(r'^wss?://', connect_to):
         print('Invalid format for websocket connection.',
-              f'\nExpected: ws://...  Got: {connect_to}\n')
+              f'\nExpected either wss://... or ws://...  Got: {connect_to}\n')
         print(usage)
         sys.exit(1)  # abort because of error
 
     # Verify that the port is specified.
     if not re.search(r'\:\d+$', connect_to):
-        print('Invalid format for websocket connection.  Connection expected ',
-              'to end with \nport specifier (colon followed by digits), ',
-              'e.g., ws://s2.ripple.com:443')
+        print('Invalid format for websocket connection. Connection expected',
+              'to end with \nport specifier (colon followed by digits),',
+              'e.g., wss://s2.ripple.com:443')
         print(f'Got: {connect_to}\n')
         print(usage)
         sys.exit(1)  # abort because of error
@@ -97,7 +99,10 @@ def get_response(ws, req_id):
         json_msg = json.loads(msg)
         got_id = json_msg["id"]
         if got_id == req_id:
+            # Remove the websocket id to reduce noise.
+            json_msg.pop("id", None)
             return json_msg
+
         print(
             f"Unexpected websocket message id: {got_id}.  Expected {req_id}.")
 
@@ -116,15 +121,12 @@ def print_account_info(ws, account_id):
 
     ws.send(json.dumps(cmd))
     json_msg = get_response(ws, wsid)
-
-    # Remove the websocket id to reduce noise.
-    json_msg.pop("id", None)
     print(json.dumps(json_msg, indent=4))
     return json_msg
 
 
 def print_tx(ws, txn_id):
-    '''Request tx by tnx_id, print it, and return the tx as JSON'''
+    '''Request tx by txn_id, print it, and return the tx as JSON'''
     wsid = ws_id()
 
     cmd = {
@@ -135,9 +137,6 @@ def print_tx(ws, txn_id):
 
     ws.send(json.dumps(cmd))
     json_msg = get_response(ws, wsid)
-
-    # Remove the websocket id from what we print to reduce noise.
-    json_msg.pop("id", None)
     print(json.dumps(json_msg, indent=4))
     return json_msg
 
@@ -155,9 +154,10 @@ def get_prev_txn_id(json_msg, account_id):
                   "Does your server have enough history? Unexpected stop.")
             return ""
 
-        err = json_msg["error"]
-        if json_msg['error_message']:
-            err = json_msg['error_message']
+        if "error_message" in json_msg:
+            err = json_msg["error_message"]
+        else:
+            err = json_msg["error"] + "."
 
         print(f"{err}  Unexpected stop.")
         return ""
@@ -170,19 +170,19 @@ def get_prev_txn_id(json_msg, account_id):
         return ""
 
     for node in affected_nodes:
+        # Look for the next transaction.
+        try:
+            if node["ModifiedNode"]["FinalFields"]["Account"] == account_id:
+                return node["ModifiedNode"]["PreviousTxnID"]
+        except KeyError:
+            pass  # If the field is not found that's okay.
+
         # If we find the account being created then we're successfully done.
         try:
             if node["CreatedNode"]["LedgerEntryType"] == "AccountRoot":
                 if node["CreatedNode"]["NewFields"]["Account"] == account_id:
                     print(f'Created Account {account_id}.  Done.')
                     return ""
-        except KeyError:
-            pass  # If the field is not found that's okay.
-
-        # Else look for the next transaction.
-        try:
-            if node["ModifiedNode"]["FinalFields"]["Account"] == account_id:
-                return node["ModifiedNode"]["PreviousTxnID"]
         except KeyError:
             continue  # If the field is not found try the next node.
 
@@ -208,8 +208,8 @@ def thread_account(ws, account_id):
           this account.
        3. Call tx with that TxId.  Save that transaction.
        4. The tx response should contain the AccountRoot for the account_id.
-          Extract the new value of PreviousTxID from the AccountRoot.
-       5. Return to step 3, but using the new PreviousTxID.'''
+          Extract the new value of PreviousTxnID from the AccountRoot.
+       5. Return to step 3, but using the new PreviousTxnID.'''
 
     # Call account_info to get our starting txId.
     json_msg = print_account_info(ws, account_id)