def test_process(self, mock_sendtoaddress, mock_sleep): # a successfull run Transaction(miner=self.miner1, amount=30, network="test").save() self.miner1.update_balance() self.miner1.save() Transaction(miner=self.miner1, amount=10, network="test").save() send_autopayments("test") miner1 = Miner.objects.get(pk=self.miner1.id) self.assertEqual(miner1.balance, 0) self.assertEqual(Transaction.objects.filter(category='TX').count(), 1) tx = Transaction.objects.get(category='TX') self.assertEqual(tx.amount, -44) self.assertEqual(tx.miner.id, miner1.id) self.assertEqual(tx.network, "test") # even with a new balance, should not pay again, as we do not pay so early again Transaction(miner=self.miner1, amount=16, network="test").save() self.miner1.update_balance() self.miner1.save() send_autopayments("test") miner1 = Miner.objects.get(pk=self.miner1.id) self.assertEqual(miner1.balance, 16) self.assertEqual(Transaction.objects.filter(category='TX').count(), 1) tx = Transaction.objects.get(category='TX') self.assertEqual(tx.amount, -44) self.assertEqual(tx.miner.id, miner1.id) self.assertEqual(tx.network, "test") # but if first_trans is older, then the second one should be paid tx.inserted_at = timezone.now() - datetime.timedelta(hours=18) tx.save() send_autopayments("test") miner1 = Miner.objects.get(pk=self.miner1.id) self.assertEqual(miner1.balance, 0) self.assertEqual(Transaction.objects.filter(category='TX').count(), 2) tx = Transaction.objects.filter(category='TX').order_by('-id')[0] self.assertEqual(tx.amount, -16) self.assertEqual(tx.miner.id, miner1.id) self.assertEqual(tx.network, "test")
def setUp(self): # the miner 1 starts with a fake balance self.miner1 = Miner(network="test", balance=20, address="B91RjV9UoZa5qLNbWZFXJ42sFWbJCyxxxx") self.miner1.save() # as the user above, but with not enough balance self.miner2 = Miner(network="test", balance=1, address="B91RjV9UoZa5qLNbWZFXJ42sFWbJCyxxx2") self.miner2.save() Transaction(miner=self.miner1, amount=4, network="test").save() Transaction(miner=self.miner2, amount=1, network="test").save()
def test_process_failed(self, mock_sendtoaddress): mock_sendtoaddress.side_effect = Exception() # a successfull run Transaction(miner=self.miner1, amount=30, network="test").save() self.miner1.update_balance() self.miner1.save() Transaction(miner=self.miner1, amount=10, network="test").save() send_autopayments("test") miner1 = Miner.objects.get(pk=self.miner1.id) self.assertEqual(miner1.balance, 34) # transaction wasn't done self.assertEqual(Transaction.objects.filter(category='TX').count(), 0) self.assertEqual(TransactionError.objects.all().count(), 1)
def test_outdated_value(self, mock_sendtoaddress, mock_sleep): # balance is high enough, but not right Transaction(miner=self.miner1, amount=10, network="test").save() send_autopayments("test") miner1 = Miner.objects.get(pk=self.miner1.id) self.assertEqual(miner1.balance, 0) self.assertEqual(Transaction.objects.filter(category='TX').count(), 1) tx = Transaction.objects.get(category='TX') self.assertEqual(tx.amount, -14) self.assertEqual(tx.miner.id, miner1.id) self.assertEqual(tx.network, "test")
def setUp(self): self.miner1 = Miner(network="test", address="B91RjV9UoZa5qLNbWZFXJ42sFWbJCyxxxx") self.miner1.save() self.miner2 = Miner(network="test", address="B91RjV9UoZa5qLNbWZFXJ42sFWbJCyxxx2") self.miner2.save() Transaction(miner=self.miner1, network="test", amount=10).save() Transaction(miner=self.miner1, network="test", amount=5).save() Transaction(miner=self.miner1, network="test", amount=-2).save() # should not happen, but better safe then sorry Transaction(miner=self.miner1, network="main", amount=100).save() Transaction(miner=self.miner2, network="test", amount=99).save() Transaction(miner=self.miner2, network="test", amount=-90).save()
def setUp(self): self.miner1 = Miner(network="test", address="B91RjV9UoZa5qLNbWZFXJ42sFWbJCyxxxx") self.miner1.save() self.worker1 = Worker(miner=self.miner1) self.worker1.save() self.miner2 = Miner(network="test", address="B91RjV9UoZa5qLNbWZFXJ42sFWbJCyxxx2") self.miner2.save() self.worker2 = Worker(miner=self.miner2) self.worker2.save() self.insert_age = timezone.now() - datetime.timedelta(days=3) Transaction(miner=self.miner2, amount=60, network="test").save() # test network #---------------- # valid block1 = Block(height=1, network='test', process_status='BP', pool_block=True, subsidy=100) block1.save() # not a pool block block2 = Block(height=2, network='test', pool_block=False, subsidy=1) block2.save() # wrong status block3 = Block(height=3, network='test', process_status='OP', pool_block=True, subsidy=1) block3.save() Block.objects.all().update(inserted_at=self.insert_age) # not old enough block4 = Block(height=4, network='test', process_status='BP', pool_block=True, subsidy=1) block4.save() work_test1 = Work(worker=self.worker1, ip="1.1.1.1") work_test1.save() work_test2 = Work(worker=self.worker1, ip="1.1.1.1") work_test2.save() Solution(network='test', block=block1, bible_hash="1", miner=self.miner1, work=work_test1).save() Solution(network='test', block=block1, bible_hash="2", miner=self.miner1, work=work_test1).save() Solution(network='test', block=block1, bible_hash="3", miner=self.miner1, work=work_test1).save() Solution(network='test', block=block1, bible_hash="4", miner=self.miner2, work=work_test1).save() Solution(network='test', block=block1, bible_hash="4.5", miner=self.miner2, work=work_test1).save() Solution(network='test', block=block2, bible_hash="5", miner=self.miner1, work=work_test1).save() Solution(network='test', block=block3, bible_hash="6", miner=self.miner1, work=work_test1).save() Solution(network='test', block=block4, bible_hash="7", miner=self.miner1, work=work_test1).save() # main network # ----------------- block1 = Block(height=1, network='main', process_status='BP', pool_block=True, subsidy=1) block1.save() Block.objects.filter(network="main").update( inserted_at=self.insert_age) work_main = Work(worker=self.worker1, ip="1.1.1.1") work_main.save() Solution(network='main', block=block2, bible_hash="8", miner=self.miner1, work=work_main).save()
def shareout_next_block(network, block_height=None, dry_run=False): """ Tries to find the oldest block that had matured and was not yet processed for shares. Optional, a specific block can be given. This task takes this block, calculates the shares of all users that contributed to the block and creates the transactions with there bbps. Important: The bbp will not send here, this is an extra step, done when the transaction limit is reached for a user Dry run will not save anything to the database. """ if settings.TASK_DEBUG: print("Debug | ", "Shareout started with network", network, "on block ", block_height, " in dry-run", dry_run) # we only start here, if no block is currently processed # not important in dry_run if not dry_run and Block.objects.filter( network=network, pool_block=True, process_status='PS').count() > 0: if settings.TASK_DEBUG: print("Debug | ", "Another block is currently processed/has status 'PS'") return # try to find the oldest block then is OLDER then 1 day # (so it had matured -> the network had accepted it) and take it age = timezone.now() - datetime.timedelta( hours=settings.POOL_BLOCK_MATURE_HOURS[network]) if block_height is None: lowest_pool_waiting_height = Block.objects.filter( network=network, pool_block=True, process_status='BP', inserted_at__lt=age).aggregate(Min('height'))['height__min'] if lowest_pool_waiting_height is None: # no unprocessed block found return else: lowest_pool_waiting_height = block_height block = Block.objects.get(network=network, height=lowest_pool_waiting_height) if settings.TASK_DEBUG: print("Debug | ", "Loaded block from database:", block.height, block.network, block.process_status, block.inserted_at) # before we start, we mark the block as in-process of the shares with transaction.atomic(): block.process_status = 'PS' # Processing shares if not dry_run: block.save() # if we found a block, we ensure that is still ours! client = BiblePayRpcClient(network=network) subsidy = client.subsidy(lowest_pool_waiting_height) if settings.TASK_DEBUG: print("Debug | ", "Requested subsidy: ", subsidy) if subsidy.get('recipient') != settings.POOL_ADDRESS[network]: block.process_status = 'ST' # stale if not dry_run: block.save() return # now we count the users solutions for the block # Important: Do not remove the ".order_by()", as it is required, or the result # will be wrong (seems to be a django problem) qs = Solution.objects.filter(network=network, block=block, ignore=False) if not dry_run: # in reall live, we only want entries that where not already processed qs = qs.filter(processed=False) solution_counts = qs.values('miner_id').annotate( total=Count('miner_id')).order_by() # with that, we first calculate the total amount of relevant shares total_count = 0 for solution_count in solution_counts: total_count += solution_count['total'] if settings.TASK_DEBUG: print("Debug | ", "Solution count is ", total_count) print("Debug | ", "Miner/solution count is ", len(solution_counts)) # if there are no shares, then there is nothing todo here anymore if total_count == 0: block.process_status = 'FI' # finished if not dry_run: block.save() return # same if the block.subsidy is buggy if block.subsidy == 0: block.process_status = 'FI' # finished if not dry_run: block.save() return # before we do the share calculation, we substract the fee from the subsidy miner_subsidy = block.subsidy if settings.POOL_FEE_PERCENT > 0: miner_subsidy = calculate_miner_subsidy(block.subsidy) if settings.TASK_DEBUG: print("Debug | ", "Miner subsidy is ", miner_subsidy) # calculcation of the amount of bbp per user based on the solutions of the block subsidy_per_solution = miner_subsidy / total_count if settings.TASK_DEBUG: print("Debug | ", "Subsidy per solution is ", subsidy_per_solution, "with a total of", (subsidy_per_solution * total_count)) added_amount = 0 # and now we add the transactions that add the coins to the user # they are not send to the user here, that will be done by a different script with transaction.atomic(): for solution_count in solution_counts: user_subsidy = subsidy_per_solution * solution_count['total'] inote = 'BLOCK:' + str(block.height) + '|SOLUTIONS:' + str( solution_count['total']) tx = Transaction( network=network, miner_id=solution_count['miner_id'], amount=user_subsidy, category='MS', # some notes for the user and some for us note='Share for block %s' % block.height, internal_note=inote, ) if not dry_run: tx.save() added_amount += user_subsidy # last but not least, we mark the block and the solutions as processed if not dry_run: Solution.objects.filter(network=network, processed=False, block=block).update(processed=True) # and we mark the block as finished block.process_status = 'FI' # finished if not dry_run: block.save() if settings.TASK_DEBUG: print("Debug | ", "Created transaction with a total amount of ", added_amount) # last, we update the user balances. We do this in a new step, as the step before is more important # and should be done as fast as possible # We do not load the miner from the database, we only update it. This is faster if not dry_run: update_list = [] with transaction.atomic(): for solution_count in solution_counts: value = Miner.calculate_miner_balance( network, solution_count['miner_id']) Miner.objects.filter(pk=solution_count['miner_id']).update( balance=value)
def send_autopayments(network): """ Looks into the miner list and finds all miners that have a balance greater than settings.POOL_MINIMUM_AUTOSEND """ # no autosend is done if the miner has a balance below # this value minimum = settings.POOL_MINIMUM_AUTOSEND[network] # miners of the network with more then the minimum balance miners = Miner.objects.filter(balance__gt=minimum, network=network).values('id', 'address') client = BiblePayRpcClient(network=network) # the system will only do the transactions for # this amount of miners before this task is closed # the next task will go on from then on max_miners = 10 miner_pos = 0 for miner in miners: # we only send payments for miners who hadn't a payment in the last 12 hours last_trans_dt = timezone.now() - datetime.timedelta(hours=12) last_trans_count = Transaction.objects.filter( miner_id=miner['id'], network=network, category='TX', inserted_at__gt=last_trans_dt).count() if last_trans_count > 0: continue miner_pos += 1 with transaction.atomic(): # for security reasons, we recalculate the balance, # as the miner table can be changed by the frontend value = Miner.calculate_miner_balance(network, miner['id']) # if all is fine, we send out the bbp if value > minimum: tx_id = None try: tx_id = client.sendtoaddress(miner['address'], value) except Exception as e: error_message = ' # '.join([str(e), str(type(e))]) + "\n" error_message += str(traceback.extract_stack()) + "\n" error_message += str(traceback.format_stack()) # we should log the error if something goes wrong transerror = TransactionError( network=network, miner_id=miner['id'], amount=(value * -1), error_message=error_message, ) transerror.save() # transaction failed? Then we skip this user this time if tx_id is None: continue internal_note = 'TX_ID:%s' % tx_id tx = Transaction( network=network, miner_id=miner['id'], category='TX', # outgoing transaction # We substract the amount here, so it must be negative (55 -> -55) amount=(value * -1), # biblepay transaction tx=tx_id, # some notes for the user and some for us note='Autosend', internal_note=internal_note, ) tx.save() # no value should be on the account anymore # but we better calculate it, if something had changed value = Miner.calculate_miner_balance(network, miner['id']) # added to prevent a problem with to fast and many transactions # in a short time time.sleep(5) # we update the miner with the new balance # even if no bbp was send (because of wrong values in the db). We # at least fix the value then Miner.objects.filter(pk=miner['id'], network=network).update(balance=value) # we end after we reached our max for this task if miner_pos == max_miners: break
def test_error_on_send(self, mock_sendtoaddress): mock_sendtoaddress.side_effect = Exception() Transaction(miner=self.miner1, amount=10, network="test").save() send_autopayments("test")