Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
270 changes: 267 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,275 @@ 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, 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.exception("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.exception(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,
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.exception(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