diff --git a/sales/tasks/cleanup.py b/sales/tasks/cleanup.py index f2552de..1a6551e 100644 --- a/sales/tasks/cleanup.py +++ b/sales/tasks/cleanup.py @@ -3,6 +3,7 @@ from huey import crontab from huey.contrib.djhuey import periodic_task from sales.models import ItemSale + logger = getLogger('django.server') @periodic_task(crontab(minute='0', hour='*/12')) diff --git a/sales/tasks/notifications.py b/sales/tasks/notifications.py index 98fa2ce..1b8b34c 100644 --- a/sales/tasks/notifications.py +++ b/sales/tasks/notifications.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from huey import crontab from huey.contrib.djhuey import periodic_task from core.helpers.email_template import EmailTemplate @@ -6,7 +6,7 @@ from core.models import UserShippingAddress from sales.models import ItemSale -logger = logging.getLogger('django.server') +logger = getLogger('django.server') @periodic_task(crontab(minute='*')) def notify_buyer_of_pending_sale(): @@ -52,3 +52,17 @@ def notify_buyer_of_shipment_confirmation(): logger.info(f'[INFO] Buyer shipping info wiped for sale #{sale.id}') sale.buyer_notified_of_shipment = True sale.save() + +@periodic_task(crontab(minute='*')) +def notify_seller_of_sale_completed(): + item_sales = ItemSale.objects.filter(seller_paid=True).filter(seller_notified_of_payout=False) + for sale in item_sales: + logger.info(f'[INFO] Sale completed for #{sale.id}, notifying seller along with payout details.') + email_template = EmailTemplate( + item=sale, + scenario='sale_completed', + role='seller' + ) + email_template.send() + sale.seller_notified_of_payout = True + sale.save() diff --git a/sales/tasks/payments.py b/sales/tasks/payments.py index 9013a91..a9a5894 100644 --- a/sales/tasks/payments.py +++ b/sales/tasks/payments.py @@ -4,44 +4,54 @@ from huey import crontab from huey.contrib.djhuey import periodic_task from django.conf import settings from django.core.cache import cache -from core.monero import AuctionWallet +from core.monero import AuctionWallet, AuctionDaemon from sales.models import ItemSale logger = logging.getLogger('django.server') -@periodic_task(crontab(minute='*/3')) -def poll_for_buyer_escrow_payments(): - aw = AuctionWallet() - if aw.connected is False: - logging.error('[ERROR] Auction wallet is not connected. Quitting.') +def connect_rpc(rpc_type): + if rpc_type == "daemon": + rpc = AuctionDaemon() + elif rpc_type == "wallet": + rpc = AuctionWallet() + else: + logger.error('[ERROR] Invalid RPC type specified. Use "daemon" or "wallet"') + return False + if rpc.connected is False: + logging.error(f'[ERROR] Auction {rpc_type} is not connected. Stopping task.') return False + return rpc +@periodic_task(crontab(minute='*/2')) +def poll_for_buyer_escrow_payments(): + wallet_rpc = connect_rpc("wallet") item_sales = ItemSale.objects.filter(payment_received=False) for sale in item_sales: logger.info(f'[INFO] Polling escrow address #{sale.escrow_account_index} for sale #{sale.id} for new funds.') - sale_account = aw.wallet.accounts[sale.escrow_account_index] - unlocked = sale_account.balances()[1] - sale.received_payment_xmr = unlocked - if unlocked >= Decimal(str(sale.expected_payment_xmr)): - logger.info(f'[INFO] Found payment of {sale.received_payment_xmr} XMR for sale #{sale.id}.') - sale.payment_received = True + sale_account = wallet_rpc.wallet.accounts[sale.escrow_account_index] + tx_in = sale_account.incoming() + balances = sale_account.balances() + sale.received_payment_xmr = balances[0] + if balances[0] >= Decimal(str(sale.expected_payment_xmr)) and tx_in: + logger.info(f'[INFO] Found incoming transaction {tx_in[0].transaction} of {sale.received_payment_xmr} XMR for sale #{sale.id}.') + if tx_in[0].transaction.confirmations >= settings.BLOCK_CONFIRMATIONS_RCV: + logger.info(f'[INFO] The incoming transaction has {settings.BLOCK_CONFIRMATIONS_RCV} confirmations and enough funds. Marking payment received.') + sale.payment_received = True + else: + logger.info(f'[INFO] The incoming transaction only has {tx_in[0].transaction.confirmations} confirmations. Not enough to proceed.') sale.save() -@periodic_task(crontab(minute='*/3')) +@periodic_task(crontab(minute='*/4')) def pay_sellers_on_sold_items(): - aw = AuctionWallet() - if aw.connected is False: - logging.error('[ERROR] Auction wallet is not connected. Quitting.') - return False - + wallet_rpc = connect_rpc("wallet") item_sales = ItemSale.objects.filter(item_received=True, payment_received=True).filter(seller_paid=False) for sale in item_sales: # Take platform fees from the sale - the 50:50 split between buyer/seller sale_total = sale.agreed_price_xmr - sale.platform_fee_xmr - sale_account = aw.wallet.accounts[sale.escrow_account_index] - + sale_account = wallet_rpc.wallet.accounts[sale.escrow_account_index] + logger.info(f'[INFO] Seller needs to be paid for sale #{sale.id}. Found balances of {sale_account.balances()} in account #{sale.escrow_account_index}.') if sale_account.balances()[1] >= Decimal(str(sale.agreed_price_xmr)): try: # Construct a transaction so we can get current fee and subtract from the total @@ -64,31 +74,17 @@ def pay_sellers_on_sold_items(): else: logger.warning(f'[WARNING] Not enough unlocked funds available in account #{sale.escrow_account_index} for sale #{sale.id}.') - if sale.seller_paid and sale.seller_notified_of_payout is False: - email_template = EmailTemplate( - item=sale, - scenario='sale_completed', - role='seller' - ) - sent = email_template.send() - sale.seller_notified_of_payout = True - sale.save() - @periodic_task(crontab(minute='*/30')) def pay_platform_on_sold_items(): - aw = AuctionWallet() - if aw.connected is False: - logging.error('[ERROR] Auction wallet is not connected. Quitting.') - return False - + wallet_rpc = connect_rpc("wallet") aof = settings.PLATFORM_WALLET_ADDRESS if aof is None: - aof = str(aw.wallet.accounts[0].address()) + aof = str(wallet_rpc.wallet.accounts[0].address()) item_sales = ItemSale.objects.filter(escrow_complete=True, seller_paid=True, item_received=True).filter(platform_paid=False) for sale in item_sales: logger.info(f'[INFO] Paying platform fees for sale #{sale.id} to wallet {aof}.') - sale_account = aw.wallet.accounts[sale.escrow_account_index] + sale_account = wallet_rpc.wallet.accounts[sale.escrow_account_index] bal = sale_account.balances()[1] if bal >= 0: try: @@ -114,16 +110,12 @@ def refund_buyers_on_cancelled_sales() -> bool: :rtype: bool """ - aw = AuctionWallet() - if aw.connected is False: - logging.error('[ERROR] Auction wallet is not connected. Quitting.') - return False - + wallet_rpc = connect_rpc("wallet") item_sales = ItemSale.objects.filter(sale_cancelled=True, payment_refunded=False) for sale in item_sales: logger.info(f'[INFO] Refunding any sent funds from the buyer for sale #{sale.id}.') sale_total = sale.agreed_price_xmr - sale.platform_fee_xmr - sale_account = aw.wallet.accounts[sale.escrow_account_index] + sale_account = wallet_rpc.wallet.accounts[sale.escrow_account_index] balances = sale_account.balances() logger.info(f'[INFO] Found balances of {balances} XMR for sale #{sale.id}.') if balances[0] != balances[1]: diff --git a/web/templates/sales/get_sale/item_received.html b/web/templates/sales/get_sale/item_received.html index 5ba11ee..2412402 100644 --- a/web/templates/sales/get_sale/item_received.html +++ b/web/templates/sales/get_sale/item_received.html @@ -15,6 +15,7 @@

The buyer confirmed receipt of their shipment which means things worked out. It's time for you to get paid!

Your payout address provided during item creation will be paid the accepted bid amount out of the escrow wallet. Please note, transaction fees will come out of the payout total if there are no platform fees!


+

Payout Sent: {{ sale.seller_paid }}

Accepted Bid (XMR): {{ sale.bid.bid_price_xmr }}

Platform Fee (XMR): {{ sale.platform_fee_xmr }}

Network Fee (XMR): {{ sale.network_fee_xmr }}

diff --git a/xmrauctions/settings.py b/xmrauctions/settings.py index 2db9b5c..5ba1f04 100644 --- a/xmrauctions/settings.py +++ b/xmrauctions/settings.py @@ -27,6 +27,7 @@ DEBUG = os.environ.get('DEBUG', False) ALLOWED_HOSTS = str(os.environ['ALLOWED_HOSTS']).split(',') ESCROW_PERIOD_DAYS = os.environ.get('ESCROW_PERIOD_DAYS', 30) PLATFORM_FEE_PERCENT = os.environ.get('PLATFORM_FEE_PERCENT', 0) +BLOCK_CONFIRMATIONS_RCV = os.environ.get('BLOCK_CONFIRMATIONS_RCV', 3) # Application definition