def processUnsent(): unsentTipFailureCount = len(unsentTipFailures) unsentTipSuccessCount = len(unsentTipSuccesses) if (unsentTipSuccessCount == 0 and unsentTipSuccessCount == 0): return tipbitUtilities.ConsolePrint( 'Attempting to process unsent. [{} | {}]'.format( unsentTipFailureCount, unsentTipSuccessCount)) while (len(unsentTipFailures) > 0): failure = unsentTipFailures[0] if (CommentReply_TipFailure(failure[0], failure[1], failure[2], False) is False): break tipbitWindow.AddEventString( 'Successfully sent a previously blocked comment') unsentTipFailures.remove(failure) while len(unsentTipSuccesses) > 0: success = unsentTipSuccesses[0] if (CommentReply_TipSuccess(success[0], success[1], success[2], success[3], success[4], success[5], False) is False): break tipbitWindow.AddEventString( 'Successfully sent a previously blocked comment') unsentTipSuccesses.remove(success)
def CommentReply_TipSuccess(comment, senderUsername, targetUsername, amountSatoshis, amountUSD, botInfoLink, firstTry=True): amountMBTC = tipbitUtilities.SatoshisToMBTC(amountSatoshis) try: comment.reply( messageTemplates.SUCCESSFUL_TIP_TEXT.format( senderUsername, targetUsername, amountMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), "%.2f" % amountUSD, botInfoLink)) return True except praw.exceptions.APIException as ex: if (firstTry is True): tipbitUtilities.ConsolePrint( 'Saving off tip success comment due to exception: {}'.format( ex.error_type)) successComment = (comment, senderUsername, targetUsername, amountSatoshis, amountUSD, botInfoLink) unsentTipSuccesses.append(successComment) return False except: tipbitWindow.AddEventString( 'Unknown error occurred while commenting on a successful tip...') return False
def SendBitcoin(senderKey, targetAddress, amount, estimatedFee, satoshisPerByte, transactionReason): amountMinusFee = amount - estimatedFee try: tipbitWindow.AddEventString( 'Sending {} Satoshis'.format(amountMinusFee)) tx = senderKey.send([(targetAddress, amountMinusFee, 'satoshi')], satoshisPerByte, botSpecificData.PRIMARY_STORAGE_ADDRESS) tipbitWindow.AddEventString('Transaction successful: {}'.format(tx)) return tx except Exception as ex: amountMBTC = tipbitUtilities.SatoshisToMBTC(amount) estimatedFeeMBTC = tipbitUtilities.SatoshisToMBTC(estimatedFee) amountMinusFeeMBTC = tipbitUtilities.SatoshisToMBTC(satoshisPerByte) tipbitWindow.AddEventString( '{} transaction of {} mBTC failed ({} + {} fee)'.format( transactionReason, amountMBTC, amountMinusFeeMBTC, estimatedFeeMBTC)) tipbitUtilities.ConsolePrint( "An exception of type {0} occurred.".format(type(ex).__name__)) tipbitUtilities.ConsolePrint(ex.args) return ""
def processComments(): # Recheck the comment for the bot name to ensure it still exists (could be a comment reply) for comment in unreadMentions: # Ensure the comment mention was in the allowed subs. Can't have this thing leaking... subName = comment.subreddit.display_name.lower() if (subName in botSpecificData.BOT_TEST_SUBS): # Process the comment if the bot is mentioned if (((botSpecificData.BOT_USERNAME + ' ') in comment.body.lower())): processSingleComment(comment, comment.body.lower()) else: tipbitWindow.AddEventString( 'We received a comment in another subreddit: {}'.format( subName)) unreadMentions.remove(comment)
def RegisterNewUser(username, isMessage, quickReg=False): username = username.lower() alreadyRegistered = isUserRegistered(username) if alreadyRegistered is False: CreateUserData(username) balance = getUserBalance(username) balanceMBTC = tipbitUtilities.SatoshisToMBTC(balance) if ((alreadyRegistered is False) and (isMessage is False)): reddit.redditor(username).message( 'Registration', messageTemplates.USER_AUTO_REGISTRATION_MESSAGE_TEXT.format( userDepositAddressesSegwit[username], userDepositAddressesSegwit[username], userDepositAddressesLegacy[username], userDepositAddressesLegacy[username])) if quickReg: return True # PM the registered user with their balance and deposit address. Mention if they were already registered and attempted to register by PM balance = getUserBalance(username) balanceMBTC = tipbitUtilities.SatoshisToMBTC(balance) if (isMessage is True): tipbitWindow.AddEventString( "Registration message processed: {} {}".format( username, ("(already registered)" if alreadyRegistered else ""))) if (alreadyRegistered is True): reddit.redditor(username).message( 'Registration', messageTemplates.USER_ALREADY_REGISTERED_REPLY_TEXT.format( userDepositAddressesSegwit[username], userDepositAddressesSegwit[username], userDepositAddressesLegacy[username], userDepositAddressesLegacy[username])) else: reddit.redditor(username).message( 'Registration', messageTemplates.USER_NEW_REGISTRATION_REPLY_TEXT.format( userDepositAddressesSegwit[username], userDepositAddressesSegwit[username], userDepositAddressesLegacy[username], userDepositAddressesLegacy[username]))
def CommentReply_TipFailure(comment, commentTemplate, targetUsername, firstTry=True): try: comment.reply(commentTemplate.format(targetUsername)) return True except praw.exceptions.APIException as ex: if (firstTry is True): tipbitUtilities.ConsolePrint( 'Saving off tip failure comment due to exception: {}'.format( ex.error_type)) failureComment = (comment, commentTemplate, targetUsername) unsentTipFailures.append(failureComment) return False except: tipbitWindow.AddEventString( 'Unknown error occurred while commenting on a successful tip...') return False
def processMessages(): # Hunt through the different subject lines we respond to. If it is anything else, toss it and mention it in console for message in unreadMessages: messageSubject = message.subject.upper().strip() messageAuthor = message.author messageAuthor = ('Name Unknown!' if (messageAuthor is None) else message.author.name.lower()) if (messageSubject == 'REGISTER'): RegisterNewUser(messageAuthor, True) elif (messageSubject == 'SWEEP DEPOSIT'): ProcessSweepDeposit(message) elif (messageSubject == 'WITHDRAW'): ProcessWithdraw(message, True) elif (messageSubject == 'WITHDRAW TEST'): ProcessWithdraw(message, False) elif (messageSubject == 'BALANCE'): ProcessBalance(messageAuthor) else: tipbitWindow.AddEventString( "Removing unknown message: {} (subject \"{}\")".format( message, messageSubject)) unreadMessages.remove(message)
def gatherUnreads(): global allUnread global markedRead allUnread = reddit.inbox.unread(limit=1) try: for item in allUnread: if (item in markedRead): tipbitWindow.AddEventString( "reddit.inbox.unread is returning items after they're marked read... something is wrong" ) continue try: if isinstance(item, Message): unreadMessages.append(item) elif isinstance(item, Comment): unreadMentions.append(item) except urllib3.exceptions.ReadTimeoutError: tipbitWindow.AddEventString( 'ReadTimeoutError on processing of unread messages and comments...' ) except ssl.SSLError: tipbitWindow.AddEventString( 'SSL error on processing of unread messages and comments...' ) except Exception as e: tipbitUtilities.ConsolePrint(e) tipbitWindow.AddEventString( 'Unknown exception on processing of unread messages and comments...' ) except RequestException: tipbitWindow.AddEventString( 'RequestException on processing unreads (likely a connection error)', False) return except ServerError: tipbitWindow.AddEventString( 'PrawCore ServerError on processing unreads (likely a connection error)', False) return for item in unreadMessages: markedRead.append(item) for item in unreadMentions: markedRead.append(item) reddit.inbox.mark_read(unreadMessages) reddit.inbox.mark_read(unreadMentions)
def ProcessBalance(username): # If the user is not registered, inform them of this if (username not in userBalances): reddit.redditor(username).message( 'Balance Check', messageTemplates.USER_BALANCE_NOT_REGISTERED_MESSAGE_TEXT.format( botSpecificData.BOT_REGISTER_PM_LINK)) return False balance = userBalances[username] balanceMBTC = tipbitUtilities.SatoshisToMBTC(balance) reddit.redditor(username).message( 'Balance Check', messageTemplates.USER_BALANCE_MESSAGE_TEXT.format( balanceMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), userDepositAddressesSegwit[username], userDepositAddressesSegwit[username], userDepositAddressesLegacy[username], userDepositAddressesLegacy[username])) tipbitWindow.AddEventString('User checked balance: {} [{} mBTC{}]'.format( username, balanceMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else '')))
def mainLoop(): currentTime = time.time() lastMainLoopTime = currentTime lastUnsentCheckTime = currentTime lastSolvencyCheckTime = currentTime lastDepositCheckTime = currentTime lastBitcoinValueTime = currentTime lastUpdateGUITime = currentTime while (True): try: currentTime = time.time() # Get the latest bitcoin price if (currentTime > lastBitcoinValueTime): tipbitUtilities.GetBitcoinValue() lastBitcoinValueTime = currentTime + 1800.0 UpdateGUI() # Check for the ESCAPE key, Balance Key (b), and Space Key (spacebar) tipbitUtilities.checkForInput(userBalances) # Update the window's event queue and processing loop tipbitWindow.UpdateWindow() # Run the main loop every 10.0 seconds if (currentTime < lastMainLoopTime): continue lastMainLoopTime = currentTime + 10.0 tipbitUtilities.UpdateNodeData() # Collect all unread mentions and messages gatherUnreads() # Display any change in the current unread or unsent count displayUnreadUnsentCount() # Attempt to re-post comments that failed to post if at least 10 seconds has gone by if (currentTime > lastUnsentCheckTime): processUnsent() lastUnsentCheckTime = currentTime + 30.0 # Check the next 5 messages processMessages() # Check the next 5 comments processComments() # Check for user deposits and send them to Storage after crediting account CheckForUserDeposits() # Update the GUI with user balance, storage total, solvency diff, and any events that have transpired UpdateGUI() except ConnectionError: tipbitWindow.AddEventString( "ConnectionError occurred during processing...", False) except RequestException: tipbitWindow.AddEventString( 'RequestException on processing unreads (likely a connection error)', False) except urllib3.exceptions.NewConnectionError: tipbitWindow.AddEventString( 'urllib3.exceptions.NewConnectionError occurred during processing...', False) except requests.exceptions.ConnectionError: tipbitWindow.AddEventString( 'requests.exceptions.ConnectionError occurred during processing...', False) except Exception as ex: tipbitWindow.AddEventString( 'An exception of type {} occurred.'.format(type(ex)), False)
def ProcessWithdraw(message, trueWithdrawal): global primaryStorageBalance username = message.author.name.lower() # Split the message and grab the address and amount from it, if it is formatted correctly messageBody = message.body messageSplit = messageBody.partition(' ') withdrawalAddress = messageSplit[0] amountStringMBTC = messageSplit[2].upper() userBalance = getUserBalance(username) # Allow the user to put ALL in as a value, and take the entire balance in that case if (amountStringMBTC == 'ALL'): amountStringMBTC = tipbitUtilities.SatoshisToMBTC(userBalance) print('User {} attempting to withdraw {}'.format(username, amountStringMBTC)) # Check that the address and amount are formatted correctly if (isinstance(amountStringMBTC, str) and amountStringMBTC.replace('.', '', 1).isdigit() is False): failedWithdrawalSubject = 'Failed Withdrawal' + ('' if trueWithdrawal else ' Test') tipbitWindow.AddEventString( 'Failed to withdraw \'{}\' to \'{}\''.format( amountStringMBTC, withdrawalAddress)) reddit.redditor(username).message( failedWithdrawalSubject, messageTemplates.USER_FAILED_WITHDRAW_MESSAGE_TEXT.format( amountStringMBTC, withdrawalAddress)) return False # Check that the amount is above or equal to the minimum withdrawal amount = tipbitUtilities.MBTCToSatoshis(Decimal(amountStringMBTC)) amountMBTC = tipbitUtilities.SatoshisToMBTC(amount) if (amount < botSpecificData.MINIMUM_WITHDRAWAL): tipbitWindow.AddEventString( '/u/{} failed to withdraw \'{}\' mBTC (below minimum withdrawal value)' .format(username, amountMBTC)) reddit.redditor(username).message( failedWithdrawalSubject, messageTemplates.USER_FAILED_WITHDRAW_BELOW_MINIMUM_MESSAGE_TEXT. format(amountMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), botSpecificData.MINIMUM_WITHDRAWAL, (' Testnet Bitcoins' if botSpecificData.testnet else ''))) return False # Check that the amount requested is in the user balance if (amount > userBalance): balanceMBTC = tipbitUtilities.SatoshisToMBTC(userBalance) tipbitWindow.AddEventString( '/u/{} failed to withdraw \'{}\' mBTC (insufficient balance of {})' .format(username, amountMBTC, balanceMBTC)) reddit.redditor(username).message( failedWithdrawalSubject, messageTemplates.USER_FAILED_WITHDRAW_LOW_BALANCE_MESSAGE_TEXT. format(amountMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), balanceMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''))) return False tipbitWindow.AddEventString( 'Attempting to withdraw {} mBTC. Storage balance: {} satoshis'.format( amountMBTC, primaryStorageBalance)) # Attempt to prepare the transaction so we can determine the fee sentTX = '' estimatedFee, sentTX = tipbitUtilities.SendFromAddressToAddress( botSpecificData.PRIMARY_STORAGE_ADDRESS, withdrawalAddress, tipbitUtilities.SatoshisToBTC(amount), botSpecificData.WITHDRAWAL_FEE_PER_BYTE, True) estimatedFeeMBTC = tipbitUtilities.SatoshisToMBTC(estimatedFee) # If the fee is more than the amount, we can't transfer any bitcoin if (amount <= estimatedFee): tipbitWindow.AddEventString( '/u/{} failed to withdraw \'{}\' (fee is greater than amount)'. format(username, amountMBTC)) reddit.redditor(username).message( failedWithdrawalSubject, messageTemplates.USER_FAILED_WITHDRAW_FEE_TOO_HIGH_MESSAGE_TEXT. format(amount, (' Testnet Bitcoins' if botSpecificData.testnet else ''), estimatedFeeMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''))) return False # If trueWithdrawal is False, we should just message the withdrawal data and not send the transaction if (trueWithdrawal is False): reddit.redditor(username).message( 'Withdrawal Test', messageTemplates.USER_WITHDRAWAL_TEST_MESSAGE_TEXT.format( amountMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), estimatedFeeMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''))) return False # Now we should have an estimated fee, so we can subtract that from the amount we're sending, and transfer it tipbitWindow.AddEventString( 'Sending {} satoshis and paying {} satoshis for the fee'.format( amount, estimatedFee)) result, sentTX = tipbitUtilities.SendFromAddressToAddress( botSpecificData.PRIMARY_STORAGE_ADDRESS, withdrawalAddress, tipbitUtilities.SatoshisToBTC(amount - estimatedFee), botSpecificData.WITHDRAWAL_FEE_PER_BYTE) print('Withdrawal result: {}'.format( result)) # TODO: Test failed withdrawal due to bad address amountMinusFees = amount - estimatedFee amountMinusFeeMBTC = tipbitUtilities.SatoshisToMBTC(amountMinusFees) addToUserBalance(username, amount * -1) balanceMBTC = tipbitUtilities.SatoshisToMBTC(getUserBalance(username)) tipbitWindow.AddEventString('{} withdrew {} mBTC {}({} + {} fee)'.format( username, amountMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), amountMinusFeeMBTC, estimatedFeeMBTC)) reddit.redditor(username).message( 'Your withdrawal was successful!', messageTemplates.USER_SUCCESS_WITHDRAW_MESSAGE_TEXT.format( amountMBTC, estimatedFeeMBTC, amountMinusFeeMBTC, balanceMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), sentTX)) return True
def ProcessSweepDeposit(message): username = message.author.name.lower() userAccount = reddit.redditor(username) RegisterNewUser(username, False) tipbitWindow.AddEventString('Attempting Sweep Deposit: {}'.format( message.body)) if (tipbitUtilities.ImportPrivateKey(message.body, '', True) is False): print('Failed to sweep private key "{}"'.format(message.body)) return legacyAddress = PrivateKeyTestnet( message.body).address if botSpecificData.testnet else PrivateKey( message.body).address print('legacy sweep - {}'.format(legacyAddress)) segwitAddress = tipbitUtilities.GetUnusedAddressSegwit('', legacyAddress) print('segwit sweep - {}'.format(segwitAddress)) sweepAddress = '' balanceList = tipbitUtilities.WalletBalancesList if (legacyAddress in balanceList): sweepAddress = legacyAddress elif (segwitAddress in balanceList): sweepAddress = segwitAddress if (sweepAddress == ''): print( 'Attempted sweep of empty wallet. Cancelling Sweep Deposit for {}'. format(username)) return print('Balance found at sweep address: {}'.format(sweepAddress)) sweepBalance = tipbitUtilities.BTCToSatoshis(balanceList[sweepAddress]) print('Sweep Balance: {} Satoshis'.format(sweepBalance)) if (sweepBalance < botSpecificData.MINIMUM_DEPOSIT): MINIMUM_DEPOSIT_MBTC = tipbitUtilities.SatoshisToMBTC( botSpecificData.MINIMUM_DEPOSIT) tipbitWindow.AddEventString( 'Failed to perform sweep deposit for user [{}] (balance under minimum deposit)' .format(username)) userAccount.message( 'Sweep Deposit failed!', messageTemplates. USER_FAILED_SWEEP_DEPOSIT_UNDER_MINIMUM_MESSAGE_TEXT.format( MINIMUM_DEPOSIT_MBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''))) return False # Attempt to prepare a transaction so we can determine the fee to take out so the user eats the cost depositBalance = sweepBalance storageTX = '' estimatedFee, storageTX = tipbitUtilities.SendFromAddressToAddress( sweepAddress, botSpecificData.PRIMARY_STORAGE_ADDRESS, depositBalance, botSpecificData.STORAGE_TRANSFER_FEE_PER_BYTE) newDepositDelta = depositBalance - estimatedFee depositBalanceMBTC = tipbitUtilities.SatoshisToMBTC(depositBalance) estimatedFeeMBTC = tipbitUtilities.SatoshisToMBTC(estimatedFee) newDepositDeltaMBTC = tipbitUtilities.SatoshisToMBTC(newDepositDelta) # Now we should have an estimated fee, so we can subtract that from the amount we're sending, and transfer it addToUserBalance(username, int(newDepositDelta)) balanceMBTC = tipbitUtilities.SatoshisToMBTC(getUserBalance(username)) tipbitWindow.AddEventString( 'Sweep Deposit successfully sent to storage: {} mBTC{}'.format( tipbitUtilities.SatoshisToMBTC(newDepositDelta), (' Testnet Bitcoins' if botSpecificData.testnet else ''))) userAccount.message( 'Your deposit was successful!', messageTemplates.USER_NEW_SWEEP_DEPOSIT_MESSAGE_TEXT.format( depositBalanceMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), depositBalanceMBTC, estimatedFeeMBTC, newDepositDeltaMBTC, balanceMBTC, (' Testnet Bitcoins' if botSpecificData.testnet else ''), storageTX)) tipbitUtilities.SetAddressToAccount(legacyAddress, '') tipbitUtilities.SetAddressToAccount(segwitAddress, '') # Return True so that we know to return True
def processSingleComment(comment, commentBody): commentBody = commentBody.lower() if (commentBody == ''): return # Note: This function allows you to pass in an altered comment body, which will then get processed. The comment reference is only used to reply onto # Grab the author's name and post OP's name to use for tipping and registration purposes senderName = comment.author.name.lower() PostOPName = comment.submission.author.name.lower() # If someone is trying to tip without being registered, comment on the failure and log the event if (isUserRegistered(senderName) is False): CommentReply_TipFailure( comment, messageTemplates.TIP_FAILURE_UNREGISTERED_USER.format( botSpecificData.BOT_REGISTER_PM_LINK), '') tipbitWindow.AddEventString( 'User {} failed to tip (sender not registered)'.format(senderName)) return False # Ensure that either the bot username is the beginning of the comment, or there is a space before it separate_around_bot = commentBody.partition(botSpecificData.BOT_USERNAME + ' ') if (separate_around_bot[1] != (botSpecificData.BOT_USERNAME + ' ')): tipbitWindow.AddEventString( "Caught a comment reply with no mention. This shouldn't happen...") return # Grab the different parts of the comment for later use separate_around_target = separate_around_bot[2].partition(" ") next_tip = separate_around_target[2].partition( botSpecificData.BOT_USERNAME + ' ') next_tip = (next_tip[1] + next_tip[2]) if (next_tip[1] == botSpecificData.BOT_USERNAME + ' ') else '' # Figure out the target of this tip targetName = separate_around_target[0].lower() if ('/u/' in targetName): targetName = targetName.partition('/u/')[2] if (targetName == 'op'): targetName = PostOPName print('processSingleComment() target: {}'.format(targetName)) redditor = reddit.redditor(targetName) # If this redditor isn't valid, comment on the failure and log the event if tipbitUtilities.isRedditorValid(redditor) is False: print('Check: Is {} banned on reddit? [{}]'.format( targetName, redditor)) CommentReply_TipFailure( comment, messageTemplates.USERNAME_IS_REMOVED_OR_BANNED_TEXT.format( botSpecificData.BOT_USERNAME, targetName), targetName) tipbitWindow.AddEventString( 'Failed to tip {} (non-existent account)'.format(targetName)) processSingleComment(comment, next_tip) return # Grab the amount value string separate_around_amount = separate_around_target[2].partition(' ') if ('\n' in separate_around_amount[0]): separate_around_amount = separate_around_amount[0].partition('\n') amountString = separate_around_amount[0] # Determine the amount in Satoshis. If the amount is invalid, comment on the failure and log the event amountSatoshi = getSatoshiFromAmountString(amountString) if amountSatoshi == -1: CommentReply_TipFailure(comment, messageTemplates.AMOUNT_NOT_SPECIFIED_TEXT, targetName) tipbitWindow.AddEventString( 'Failed to tip {} (unspecified amount: {})'.format( targetName, amountString)) processSingleComment(comment, next_tip) return # If the user does not have a sufficient balance to complete the tip, comment on the failure and log the event if (isBalanceSufficient(senderName, amountSatoshi) is False): CommentReply_TipFailure(comment, messageTemplates.AMOUNT_NOT_AVAILABLE_TEXT, targetName) tipbitWindow.AddEventString( 'Failed to tip {} (insufficient balance)'.format(targetName)) processSingleComment(comment, next_tip) return # If we get this far, the tip should be successful. Comment on the success, log it out, and complete the tip tipSuccess = True if (senderName is targetName) else processSingleTip( senderName, targetName, amountSatoshi) if (tipSuccess is True): tipbitWindow.AddEventString('Successful tip: {} -> {} ({})'.format( senderName, targetName, amountSatoshi)) CommentReply_TipSuccess(comment, senderName, targetName, amountSatoshi, tipbitUtilities.SatoshisToUSD(amountSatoshi), botSpecificData.BOT_INTRO_LINK) else: tipbitWindow.AddEventString('Failed to tip: {} -> {} ({})'.format( senderName, targetName, amountSatoshi)) CommentReply_TipFailure(comment, 'Unknown failure while tipping {}', targetName) # Attempt to pass another tip from the same comment into this function to be processed processSingleComment(comment, next_tip)