Build Wallet code: better separation of concerns

This commit is contained in:
mDuo13
2022-01-11 17:07:19 -08:00
parent b2668b7de9
commit e8a5847cc5

View File

@@ -19,11 +19,10 @@ class XRPLMonitorThread(Thread):
the main frame to be shown in the UI. Using a thread lets us maintain the the main frame to be shown in the UI. Using a thread lets us maintain the
responsiveness of the UI while doing work in the background. responsiveness of the UI while doing work in the background.
""" """
def __init__(self, url, gui, account, loop): def __init__(self, url, gui, loop):
Thread.__init__(self, daemon=True) Thread.__init__(self, daemon=True)
self.gui = gui self.gui = gui
self.url = url self.url = url
self.account = account
self.loop = loop self.loop = loop
def run(self): def run(self):
@@ -35,12 +34,15 @@ class XRPLMonitorThread(Thread):
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
self.loop.run_forever() self.loop.run_forever()
async def watch_xrpl(self): async def watch_xrpl_account(self, address, wallet=None):
""" """
This is the task that opens the connection to the XRPL, then handles This is the task that opens the connection to the XRPL, then handles
incoming subscription messages by dispatching them to the appropriate incoming subscription messages by dispatching them to the appropriate
part of the GUI. part of the GUI.
""" """
self.account = address
self.wallet = wallet
async with xrpl.asyncio.clients.AsyncWebsocketClient(self.url) as self.client: async with xrpl.asyncio.clients.AsyncWebsocketClient(self.url) as self.client:
await self.on_connected() await self.on_connected()
async for message in self.client: async for message in self.client:
@@ -76,7 +78,16 @@ class XRPLMonitorThread(Thread):
account=self.account, account=self.account,
ledger_index="validated" ledger_index="validated"
)) ))
if not response.is_successful():
print("Got error from server:", response)
# This most often happens if the account in question doesn't exist
# on the network we're connected to. Better handling would be to use
# wx.CallAfter to display an error dialog in the GUI and possibly
# let the user try inputting a different account.
exit(1)
wx.CallAfter(self.gui.update_account, response.result["account_data"]) wx.CallAfter(self.gui.update_account, response.result["account_data"])
if self.wallet:
wx.CallAfter(self.gui.enable_readwrite)
# Get the first page of the account's transaction history. Depending on # Get the first page of the account's transaction history. Depending on
# the server we're connected to, the account's full history may not be # the server we're connected to, the account's full history may not be
# available. # available.
@@ -163,7 +174,11 @@ class XRPLMonitorThread(Thread):
amount=xrpl.utils.xrp_to_drops(paydata["amt"]), amount=xrpl.utils.xrp_to_drops(paydata["amt"]),
destination_tag=dtag destination_tag=dtag
) )
tx_signed = await xrpl.asyncio.transaction.safe_sign_and_autofill_transaction(tx, self.gui.wallet, self.client) # Autofill provides a sequence number, but this may fail if you try to
# send too many transactions too fast. You can send transactions more
# rapidly if you track the sequence number more carefully.
tx_signed = await xrpl.asyncio.transaction.safe_sign_and_autofill_transaction(
tx, self.wallet, self.client)
await xrpl.asyncio.transaction.submit_transaction(tx_signed, self.client) await xrpl.asyncio.transaction.submit_transaction(tx_signed, self.client)
wx.CallAfter(self.gui.add_pending_tx, tx_signed) wx.CallAfter(self.gui.add_pending_tx, tx_signed)
@@ -355,11 +370,27 @@ class TWaXLFrame(wx.Frame):
def __init__(self, url, test_network=True): def __init__(self, url, test_network=True):
wx.Frame.__init__(self, None, title="TWaXL", size=wx.Size(800,400)) wx.Frame.__init__(self, None, title="TWaXL", size=wx.Size(800,400))
self.url = url
self.test_network = test_network self.test_network = test_network
# The ledger's current reserve settings. To be filled in later.
self.reserve_base = None
self.reserve_inc = None
# This account's total XRP reserve including base + owner amounts
self.reserve_xrp = None
self.build_ui()
# Pop up to ask user for their account ---------------------------------
address, wallet = self.prompt_for_account()
self.classic_address = address
# Start background thread for updates from the ledger ------------------
self.worker_loop = asyncio.new_event_loop()
self.worker = XRPLMonitorThread(url, self, self.worker_loop)
self.worker.start()
self.run_bg_job(self.worker.watch_xrpl_account(address, wallet))
def build_ui(self):
self.tabs = wx.Notebook(self, style=wx.BK_DEFAULT) self.tabs = wx.Notebook(self, style=wx.BK_DEFAULT)
# Tab 1: "Summary" pane ------------------------------------------------ # Tab 1: "Summary" pane ------------------------------------------------
main_panel = wx.Panel(self.tabs) main_panel = wx.Panel(self.tabs)
self.tabs.AddPage(main_panel, "Summary") self.tabs.AddPage(main_panel, "Summary")
@@ -382,18 +413,16 @@ class TWaXLFrame(wx.Frame):
(lbl_reserve, self.st_reserve)) ) (lbl_reserve, self.st_reserve)) )
# Send XRP button. # Send XRP button. Disabled until we have a secret key & network connection
self.sxb = wx.Button(main_panel, label="Send XRP") self.sxb = wx.Button(main_panel, label="Send XRP")
self.sxb.SetToolTip("Disabled in read-only mode.")
self.sxb.Disable() self.sxb.Disable()
self.Bind(wx.EVT_BUTTON, self.click_send_xrp, source=self.sxb)
# Ledger info text. One multi-line static text, unlike the account area. # Ledger info text. One multi-line static text, unlike the account area.
self.ledger_info = wx.StaticText(main_panel, label="Not connected") self.ledger_info = wx.StaticText(main_panel, label="Not connected")
# The ledger's current reserve settings. To be filled in when we get a
# ledger event.
self.reserve_base = None
self.reserve_inc = None
main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer = wx.BoxSizer(wx.VERTICAL)
main_sizer.Add(self.acct_info_area, 1, flag=wx.EXPAND|wx.ALL, border=5) main_sizer.Add(self.acct_info_area, 1, flag=wx.EXPAND|wx.ALL, border=5)
main_sizer.Add(self.sxb, 0, flag=wx.ALL, border=5) main_sizer.Add(self.sxb, 0, flag=wx.ALL, border=5)
@@ -418,35 +447,13 @@ class TWaXLFrame(wx.Frame):
objs_panel.SetSizer(objs_sizer) objs_panel.SetSizer(objs_sizer)
# Pop up to ask user for their account ---------------------------------
account_dialog = wx.TextEntryDialog(self,
"Please enter an account address (for read-only)"
" or your secret (for read-write access)",
caption="Enter account",
value="rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe")
account_dialog.Bind(wx.EVT_TEXT, self.toggle_dialog_style)
if account_dialog.ShowModal() == wx.ID_OK:
self.set_up_account(account_dialog.GetValue())
account_dialog.Destroy()
else:
# If the user presses Cancel on the account entry, exit the app.
exit(1)
# Attach handlers and start bg thread for updates from the ledger ------
self.Bind(wx.EVT_BUTTON, self.click_send_xrp, source=self.sxb)
self.worker_loop = asyncio.new_event_loop()
self.worker = XRPLMonitorThread(url, self, self.classic_address, self.worker_loop)
self.worker.start()
self.run_bg_job(self.worker.watch_xrpl())
def run_bg_job(self, job): def run_bg_job(self, job):
""" """
Schedules a job to run asynchronously in the XRPL worker thread. Schedules a job to run asynchronously in the XRPL worker thread.
The job should be a Future (for example, from calling an async function) The job should be a Future (for example, from calling an async function)
""" """
task = asyncio.run_coroutine_threadsafe(job, self.worker_loop) task = asyncio.run_coroutine_threadsafe(job, self.worker_loop)
def toggle_dialog_style(self, event): def toggle_dialog_style(self, event):
""" """
Automatically switches to a password-style dialog if it looks like the Automatically switches to a password-style dialog if it looks like the
@@ -459,48 +466,71 @@ class TWaXLFrame(wx.Frame):
else: else:
dlg.SetWindowStyle(wx.TE_LEFT) dlg.SetWindowStyle(wx.TE_LEFT)
def set_up_account(self, value): def prompt_for_account(self):
""" """
Set up fields for address and wallet (or quit with an error) depending Prompt the user for an account to use, in a base58-encoded format:
on what input the user provided. If the user provides an address, go - master key seed: Grants read-write access.
into "read-only" mode. Note: this app does is not capable of using (assumes the master key pair is not disabled)
regular keys or multi-signing yet. - classic address. Grants read-only access.
- X-address. Grants read-only access.
Exits with error code 1 if the user cancels the dialog, if the input
doesn't match any of the formats, or if the user inputs an X-address
intended for use on a different network type (test/non-test).
Populates the classic address and X-address labels in the UI.
Returns (classic_address, wallet) where wallet is None in read-only mode
""" """
value = value.strip() account_dialog = wx.TextEntryDialog(self,
"Please enter an account address (for read-only)"
" or your secret (for read-write access)",
caption="Enter account",
value="rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe")
account_dialog.Bind(wx.EVT_TEXT, self.toggle_dialog_style)
if account_dialog.ShowModal() != wx.ID_OK:
# If the user presses Cancel on the account entry, exit the app.
exit(1)
value = account_dialog.GetValue().strip()
account_dialog.Destroy()
classic_address = ""
wallet = None
x_address = ""
if xrpl.core.addresscodec.is_valid_xaddress(value): if xrpl.core.addresscodec.is_valid_xaddress(value):
x_address = value
classic_address, dest_tag, test_network = xrpl.core.addresscodec.xaddress_to_classic_address(value) classic_address, dest_tag, test_network = xrpl.core.addresscodec.xaddress_to_classic_address(value)
if test_network != self.test_network: if test_network != self.test_network:
on_net = "a test network" if self.test_network else "Mainnet"
print(f"X-address {value} is meant for a different network type" print(f"X-address {value} is meant for a different network type"
f"than this client is connected to." f"than this client is connected to."
f"(Client is on: {'a test network' if self.test_network else 'Mainnet'})") f"(Client is on: {on_net})")
exit(1) exit(1)
self.xaddress = value
self.classic_address = classic_address
self.wallet = None
self.sxb.SetToolTip("Disabled in read-only mode.")
elif xrpl.core.addresscodec.is_valid_classic_address(value): elif xrpl.core.addresscodec.is_valid_classic_address(value):
self.xaddress = xrpl.core.addresscodec.classic_address_to_xaddress( classic_address = value
x_address = xrpl.core.addresscodec.classic_address_to_xaddress(
value, tag=None, is_test_network=self.test_network) value, tag=None, is_test_network=self.test_network)
self.classic_address = value
self.wallet = None
self.sxb.SetToolTip("Disabled in read-only mode.")
else: else:
try: try:
# Check if it's a valid seed # Check if it's a valid seed
seed_bytes, alg = xrpl.core.addresscodec.decode_seed(value) seed_bytes, alg = xrpl.core.addresscodec.decode_seed(value)
self.wallet = xrpl.wallet.Wallet(seed=value, sequence=0) wallet = xrpl.wallet.Wallet(seed=value, sequence=0)
# We'll fill in the actual sequence later. x_address = wallet.get_xaddress(is_test=self.test_network)
self.xaddress = self.wallet.get_xaddress(is_test=self.test_network) classic_address = wallet.classic_address
self.classic_address = self.wallet.classic_address
except Exception as e: except Exception as e:
print(e) print(e)
exit(1) exit(1)
self.st_classic_address.SetLabel(self.classic_address)
self.st_x_address.SetLabel(self.xaddress)
# Update the UI with the address values
self.st_classic_address.SetLabel(classic_address)
self.st_x_address.SetLabel(x_address)
return classic_address, wallet
def update_ledger(self, message): def update_ledger(self, message):
""" """
@@ -530,8 +560,7 @@ class TWaXLFrame(wx.Frame):
def update_account(self, acct): def update_account(self, acct):
""" """
Process an account_info response to update the account info area of the Update the account info UI based on an account_info response.
UI. This also updates the sequence number of the wallet.
""" """
xrp_balance = str(xrpl.utils.drops_to_xrp(acct["Balance"])) xrp_balance = str(xrpl.utils.drops_to_xrp(acct["Balance"]))
self.st_xrp_balance.SetLabel(xrp_balance) self.st_xrp_balance.SetLabel(xrp_balance)
@@ -542,11 +571,12 @@ class TWaXLFrame(wx.Frame):
self.st_reserve.SetLabel(str(reserve_xrp)) self.st_reserve.SetLabel(str(reserve_xrp))
self.reserve_xrp = reserve_xrp self.reserve_xrp = reserve_xrp
# If we're not read-only, we can set/update the Sequence number, and def enable_readwrite(self):
# enable the Send XRP button if it isn't enabled yet. """
if self.wallet: Enable buttons for sending transactions.
self.wallet.sequence = acct["Sequence"] """
self.sxb.Enable() self.sxb.Enable()
self.sxb.SetToolTip("")
@staticmethod @staticmethod
def displayable_amount(a): def displayable_amount(a):
@@ -684,7 +714,7 @@ class TWaXLFrame(wx.Frame):
self.run_bg_job(self.worker.send_xrp(paydata)) self.run_bg_job(self.worker.send_xrp(paydata))
if __name__ == "__main__": if __name__ == "__main__":
WS_URL = "wss://s.altnet.rippletest.net:51233" WS_URL = "wss://s.altnet.rippletest.net:51233" # Testnet
app = wx.App() app = wx.App()
frame = TWaXLFrame(WS_URL, test_network=True) frame = TWaXLFrame(WS_URL, test_network=True)
frame.Show() frame.Show()