Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@ ORD_SERVER_URL="http://ipv4:port"

GITHUB_CLIENT_SECRET=be8599360ca3e1234
GITHUB_CLIENT_ID=Ov23liITA1234

#AutoPay Bounty
BCH_API_KEY=your_payment_provider_api_key
BCH_WALLET_ADDRESS=your_sponsor_wallet_address
PAYMENT_ENABLED=False
MAX_AUTO_PAYMENT=50 # Maximum amount for automatic payment
50 changes: 50 additions & 0 deletions website/bitcoin_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# bacon/bitcoin_utils.py

import logging
from decimal import Decimal

import requests
from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException
from django.conf import settings

Expand Down Expand Up @@ -58,3 +60,51 @@ def issue_asset(asset_name, amount, identifier):

# The transaction ID (txid) can be used to track the issuance on the blockchain.
return txid


def send_bch_payment(address, amount):
"""
Send BCH payment using your BCH payment provider.
Returns transaction ID.
"""

# Validate BCH address format
if not address.startswith("bitcoincash:"):
raise ValueError(f"Invalid BCH address: {address}")

# Format amount to 8 decimals (required for BCH)
formatted_amount = f"{Decimal(amount):.8f}"

url = settings.BCH_PAYMENT_API_URL

payload = {"to_address": address, "amount": formatted_amount, "currency": "BCH"}

headers = {"Authorization": f"Bearer {settings.BCH_API_KEY}", "Content-Type": "application/json"}

try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
except requests.exceptions.RequestException as e:
logger.error(f"BCH payment request failed: {str(e)}")
raise Exception("BCH network/payment provider unreachable")

# Handle non-200 result
if response.status_code != 200:
logger.error(f"BCH payment failed ({response.status_code}): {response.text}")
raise Exception(f"BCH payment failed: {response.text}")

data = response.json()

# Check if provider sent an error
if "error" in data:
logger.error(f"BCH payment error: {data['error']}")
raise Exception(f"BCH payment failed: {data['error']}")

# Ensure transaction_id exists
tx_id = data.get("transaction_id")
if not tx_id:
logger.error("BCH payment response missing transaction_id")
raise Exception("Invalid BCH payment response")

logger.info(f"BCH payment success: tx {tx_id} to {address}")

return tx_id
271 changes: 268 additions & 3 deletions website/views/user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import json
import logging
import os
import re
from datetime import datetime, timezone
from decimal import Decimal

import requests
from allauth.account.signals import user_signed_up
from django.conf import settings
from django.contrib import messages
Expand All @@ -13,6 +16,7 @@
from django.contrib.sites.shortcuts import get_current_site
from django.core.cache import cache
from django.core.mail import send_mail
from django.db import transaction
from django.db.models import Count, F, Q, Sum
from django.db.models.functions import ExtractMonth
from django.dispatch import receiver
Expand All @@ -29,7 +33,6 @@
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response

from blt import settings
from website.forms import MonitorForm, UserDeleteForm, UserProfileForm
from website.models import (
IP,
Expand All @@ -48,6 +51,7 @@
Monitor,
Notification,
Points,
Repo,
Tag,
Thread,
User,
Expand Down Expand Up @@ -132,8 +136,7 @@ def update_bch_address(request):
messages.error(request, f"Please provide a valid {selected_crypto} Address.")
else:
messages.error(request, "Invalid request method.")

username = request.user.username if request.user.username else "default_username"
username = request.user.username or "default_username"
return redirect(reverse("profile", args=[username]))


Expand Down Expand Up @@ -1011,14 +1014,276 @@ def github_webhook(request):


def handle_pull_request_event(payload):
# Enhanced to handle automatic payments for merged PRs with bounty labels
if payload["action"] == "closed" and payload["pull_request"]["merged"]:
pr_user_profile = UserProfile.objects.filter(github_url=payload["pull_request"]["user"]["html_url"]).first()
if pr_user_profile:
pr_user_instance = pr_user_profile.user
assign_github_badge(pr_user_instance, "First PR Merged")

# Check for bounty labels ($5, $10, etc.)
labels = [label["name"] for label in payload["pull_request"]["labels"]]
bounty_amount = extract_bounty_from_labels(labels)

if bounty_amount:
# Process automatic payment
process_bounty_payment(
pr_user_profile=pr_user_profile, usd_amount=bounty_amount, pr_data=payload["pull_request"]
)
return JsonResponse({"status": "success"}, status=200)


def extract_bounty_from_labels(labels):
# Extract bounty amount from PR labels like $5, $10, etc.
for label in labels:
match = re.match(r"^\$(\d+(?:\.\d+)?)$", label.strip())
if match:
return Decimal(match.group(1))
return None


def send_crypto_payment(address, amount, currency):
"""
Send cryptocurrency payment. This needs integration with:
- BitPay API for BCH
- Coinbase Commerce API
- Direct blockchain transaction via bitcoin_utils.py
- Or your preferred payment processor
"""
if currency == "BCH":
from website.bitcoin_utils import send_bch_payment

tx_id = send_bch_payment(address, str(amount))
return tx_id
raise NotImplementedError(f"Payment method {currency} not implemented")


def record_payment(pr_user_profile, pr_data, tx_id, bch_amount, currency, usd_amount):
"""
Record payment into GitHubIssue + update user winnings safely.
"""

# Extract repo name safely
try:
repo_name = pr_data["base"]["repo"]["name"]
except KeyError:
logger.error("PR data missing base.repo.name structure")
return

repo = Repo.objects.filter(name__iexact=repo_name).first()
if not repo:
logger.warning(
"Repo %s not tracked; refusing to record payment for PR #%s",
repo_name,
pr_data.get("number"),
)
return

pr_number = pr_data.get("number")

# Use atomic transaction to prevent partial writes
with transaction.atomic():
github_issue, created = GitHubIssue.objects.get_or_create(
repo=repo,
number=pr_number,
defaults={
"title": pr_data.get("title", ""),
"type": "pull_request",
"user_profile": pr_user_profile,
"is_merged": True,
"merged_at": timezone.now(),
},
)

# If issue exists but missing user_profile, set it
if not created and github_issue.user_profile is None:
github_issue.user_profile = pr_user_profile

# Add transaction IDs
if currency == "BCH":
github_issue.bch_tx_id = tx_id
else:
github_issue.sponsors_tx_id = tx_id

github_issue.save()

# Safe Decimal calculation for winnings
current_win = pr_user_profile.winnings or Decimal("0")
pr_user_profile.winnings = current_win + Decimal(str(usd_amount))
pr_user_profile.save()

logger.info(
"Recorded payment for PR #%s user=%s amount=%s USD",
pr_number,
pr_user_profile.user.username,
usd_amount,
)


def post_payment_comment(pr_data, tx_id, amount, currency):
# Post confirmation comment to the PR
if isinstance(amount, dict):
bch_amount = amount["bch"]
usd_amount = amount["usd"]
else:
bch_amount = amount
usd_amount = None

repo_full_name = pr_data["base"]["repo"]["full_name"]
pr_number = pr_data["number"]

if currency == "BCH":
explorer_url = f"https://blockchair.com/bitcoin-cash/transaction/{tx_id}"
comment_body = (
f"🎉 **Payment Sent!**\n\n"
f"${usd_amount} has been automatically sent via Bitcoin Cash (BCH).\n\n"
f"**Transaction ID:** `{tx_id}`\n"
f"**View Transaction:** {explorer_url}\n\n"
f"Thank you for your contribution! 🙏"
)
else:
comment_body = f"${usd_amount} USD ({bch_amount} BCH) has been automatically sent..."

url = f"https://api.github.com/repos/{repo_full_name}/issues/{pr_number}/comments"
headers = {"Authorization": f"token {settings.GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"}

response = requests.post(url, json={"body": comment_body}, headers=headers, timeout=10)
return response.status_code == 201


def notify_user_missing_address(user, pr_data):
# Notify user they need to add a crypto address
# Send email
send_mail(
subject="Action Required: Add BCH Address for Payment",
message=(
f"Your PR #{pr_data['number']} has been merged and is eligible for a bounty payment!\n\n"
f"However, we don't have a cryptocurrency address on file for you.\n\n"
f"Please add your BCH address (preferred) at: {settings.SITE_URL}/profile/edit/\n\n"
f"Note: BCH addresses must start with 'bitcoincash:'"
),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
)


def check_existing_payment(repo_full_name, pr_number):
repo_short = repo_full_name.split("/")[-1]
repo = Repo.objects.filter(name__iexact=repo_short).first()
if not repo:
logger.info("Repo %s not tracked; skipping payment for PR #%s", repo_full_name, pr_number)
return True # treat as already handled / ineligible

issue = GitHubIssue.objects.filter(repo=repo, number=pr_number).first()
if not issue:
return False

return bool(issue.bch_tx_id or issue.sponsors_tx_id)


def notify_admin_payment_failure(pr_data, error_message):
admin_email = settings.ADMIN_EMAIL
send_mail(
subject=f"Payment Failure for PR #{pr_data['number']}",
message=f"Error processing payment:\n\n{error_message}",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[admin_email],
)


def process_bounty_payment(pr_user_profile, usd_amount, pr_data):
"""
Process automatic bounty payment for merged PR.
Prefers BCH addresses.
"""
# --- SAFETY GATE 1: PAYMENT ENABLED ---
if not getattr(settings, "PAYMENT_ENABLED", False):
logger.info("PAYMENT_ENABLED is False; skipping autopay for PR #%s", pr_data["number"])
return

# --- MAX_AUTO_PAYMENT in USD ---
max_auto = getattr(settings, "MAX_AUTO_PAYMENT", None)
if max_auto is not None:
max_auto_dec = Decimal(str(max_auto))
if usd_amount > max_auto_dec:
logger.warning(
"USD bounty %s exceeds MAX_AUTO_PAYMENT %s; skipping autopay for PR #%s",
usd_amount,
max_auto_dec,
pr_data["number"],
)
return

try:
resp = requests.get(
"https://api.coingecko.com/api/v3/simple/price",
params={"ids": "bitcoin-cash", "vs_currencies": "usd"},
timeout=5,
)
resp.raise_for_status()
rate = Decimal(str(resp.json()["bitcoin-cash"]["usd"]))
bch_amount = usd_amount / rate
except Exception as e:
logger.error(f"Unable to fetch BCH price: {e}")
notify_admin_payment_failure(pr_data, "Unable to fetch BCH/USD rate")
return

# Step 1: Select BCH address
if pr_user_profile.bch_address:
payment_address = pr_user_profile.bch_address
payment_method = "BCH"
else:
notify_user_missing_address(pr_user_profile.user, pr_data)
logger.warning(f"User {pr_user_profile.user.username} has no crypto address for PR #{pr_data['number']}")
return

# Step 2: Validate BCH address format
if not payment_address.startswith("bitcoincash:"):
logger.error(f"Invalid BCH address: {payment_address}")
notify_admin_payment_failure(pr_data, "Invalid BCH address format")
return

# Step 3: Prevent duplicate payment
pr_number = pr_data["number"]
repo_full_name = pr_data["base"]["repo"]["full_name"]

if check_existing_payment(repo_full_name, pr_number):
logger.info(f"PR #{pr_number} already paid, skipping")
return

# Step 4: Execute payment
try:
logger.info(
f"Initiating BCH payment for PR #{pr_number}: "
f"{bch_amount:.6f} BCH to address {payment_address} "
f"for user {pr_user_profile.user.username}"
)

tx_id = send_crypto_payment(address=payment_address, amount=bch_amount, currency=payment_method)

# Step 5: Save payment record
record_payment(
pr_user_profile=pr_user_profile,
pr_data=pr_data,
tx_id=tx_id,
bch_amount=bch_amount,
currency=payment_method,
usd_amount=usd_amount,
)

# Step 6: Comment on PR
post_payment_comment(pr_data, tx_id, {"bch": bch_amount, "usd": usd_amount}, payment_method)

logger.info(
f"Successfully paid {bch_amount:.6f} {payment_method} to "
f"{pr_user_profile.user.username} for PR #{pr_number}"
)

except Exception as e:
logger.error(f"Payment failed for PR #{pr_number}: {str(e)}")
notify_admin_payment_failure(pr_data, str(e))


def handle_push_event(payload):
pusher_profile = UserProfile.objects.filter(github_url=payload["sender"]["html_url"]).first()
if pusher_profile:
Expand Down
Loading