Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: refactor + new endpoint for ETC payout #6094

Merged
merged 2 commits into from
Feb 28, 2020
Merged
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
4 changes: 2 additions & 2 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
url('^api/v1/bounty/create', dashboard.views.create_bounty_v1, name='create_bounty_v1'),
url('^api/v1/bounty/cancel', dashboard.views.cancel_bounty_v1, name='cancel_bounty_v1'),
url('^api/v1/bounty/fulfill', dashboard.views.fulfill_bounty_v1, name='fulfill_bounty_v1'),
url('^api/v1/bounty/payout/<int:fulfillment_id>', dashboard.views.payout_bounty_v1, name='payout_bounty_v1'),

# inbox
re_path(r'^inbox/?', include('inbox.urls', namespace='inbox')),
Expand Down Expand Up @@ -231,7 +232,7 @@
re_path(r'^bounty/quickstart/?', dashboard.views.quickstart, name='quickstart'),
url(r'^bounty/new/?', dashboard.views.new_bounty, name='new_bounty'),
re_path(r'^bounty/change/(?P<bounty_id>.*)?', dashboard.views.change_bounty, name='change_bounty'),
url(r'^bounty/sync_payout/(?P<bounty_id>.*)?', dashboard.views.manual_sync_etc_payout, name='manual_sync_etc_payout'),
url(r'^bounty/sync_payout/(?P<fulfillment_id>.*)?', dashboard.views.manual_sync_etc_payout, name='manual_sync_etc_payout'),
url(r'^funding/new/?', dashboard.views.new_bounty, name='new_funding'), # TODO: Remove
url(r'^new/?', dashboard.views.new_bounty, name='new_funding_short'), # TODO: Remove
# TODO: Rename below to bounty/
Expand Down Expand Up @@ -352,7 +353,6 @@

# sync methods
url(r'^sync/web3/?', dashboard.views.sync_web3, name='sync_web3'),
url(r'^sync/etc/?', dashboard.views.sync_etc, name='sync_etc'),
url(r'^sync/get_amount/?', dashboard.helpers.amount, name='helpers_amount'),
re_path(r'^sync/get_issue_details/?', dashboard.helpers.issue_details, name='helpers_issue_details'),

Expand Down
12 changes: 6 additions & 6 deletions app/dashboard/management/commands/sync_etc_payouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from django.core.management.base import BaseCommand

from dashboard.models import Bounty
from dashboard.models import BountyFulfillment
from dashboard.utils import sync_etc_payout


Expand All @@ -28,8 +28,8 @@ class Command(BaseCommand):
help = 'checks if payments are confirmed for ETC bounties that have been paid out'

def handle(self, *args, **options):
bounties_to_check = Bounties.objects.filter(
payout_tx_id=None, bounty_state='done', token_name='ETC',
network='ETC')
for bounty in bounties_to_check.all():
sync_etc_payout(bounty)
fulfillments_to_check = BountyFulfillment.objects.filter(
payout_status='pending', token_name='ETC'
)
for fulfillment in fulfillments_to_check.all():
sync_etc_payout(fulfillment)
12 changes: 10 additions & 2 deletions app/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,6 @@ class Bounty(SuperModel):
WORK_IN_PROGRESS_STATUSES = ['reserved', 'open', 'started', 'submitted']
TERMINAL_STATUSES = ['done', 'expired', 'cancelled']

payout_confirmed = models.BooleanField(default=False, blank=True, null=True)
payout_tx_id = models.CharField(default="0x0", max_length=255, blank=True)
bounty_state = models.CharField(max_length=50, choices=BOUNTY_STATES, default='open', db_index=True)
web3_type = models.CharField(max_length=50, default='bounties_network')
title = models.CharField(max_length=1000)
Expand Down Expand Up @@ -1325,6 +1323,11 @@ def submitted(self):
class BountyFulfillment(SuperModel):
"""The structure of a fulfillment on a Bounty."""

PAYOUT_STATUS = [
('pending', 'pending'),
('done', 'done'),
]

fulfiller_address = models.CharField(max_length=50)
fulfiller_email = models.CharField(max_length=255, blank=True)
fulfiller_github_username = models.CharField(max_length=255, blank=True)
Expand All @@ -1340,6 +1343,11 @@ class BountyFulfillment(SuperModel):
bounty = models.ForeignKey(Bounty, related_name='fulfillments', on_delete=models.CASCADE)
profile = models.ForeignKey('dashboard.Profile', related_name='fulfilled', on_delete=models.CASCADE, null=True)

token_name = models.CharField(max_length=10, blank=True)
payout_tx_id = models.CharField(default="0x0", max_length=255, blank=True)
payout_status = models.CharField(max_length=10, choices=PAYOUT_STATUS, blank=True)
payout_amount = models.DecimalField(null=True, blank=True, decimal_places=2, max_digits=10)

def __str__(self):
"""Define the string representation of BountyFulfillment.

Expand Down
41 changes: 24 additions & 17 deletions app/dashboard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from app.utils import sync_profile
from compliance.models import Country, Entity
from dashboard.helpers import UnsupportedSchemaException, normalize_url, process_bounty_changes, process_bounty_details
from dashboard.models import Activity, BlockedUser, Bounty, Profile, UserAction
from dashboard.models import Activity, BlockedUser, Bounty, BountyFulfillment, Profile, UserAction
from eth_utils import to_checksum_address
from gas.utils import conf_time_spread, eth_usd_conv_rate, gas_advisories, recommend_min_gas_price_to_confirm_in_time
from hexbytes import HexBytes
Expand Down Expand Up @@ -473,20 +473,28 @@ def has_tx_mined(txid, network):


def etc_txn_already_used(t):
b = Bounty.objects.filter(token_name='ETC',
network='ETC',
payout_tx_id=t['hash']).first()
return True if b else False
return BountyFulfillment.objects.filter(
payout_tx_id = t['hash'],
token_name='ETC'
).exists()


def search_for_etc_bounty_payout(bounty, payeeAddress=None, network='mainnet'):
funderAddress = bounty.bounty_owner_profile.etc_address
def search_for_etc_bounty_payout(fulfillment, network='mainnet'):
if fulfillment.token_name != 'ETC':
return

funderAddress = fulfillment.bounty.bounty_owner_address
amount = fulfillment.payout_amount
payeeAddress = fulfillment.fulfiller_address

blockscout_url = f'https://blockscout.com/etc/{network}/api?module=account&action=txlist&address={funderAddress}'
response = requests.get(blockscout_url).json()
blockscout_response = requests.get(blockscout_url).json()
if blockscout_response['message'] and blockscout_response['result']:
for t in blockscout_response['result']:
if (t['to'] == payeeAddress and t['amount'] >= bounty.value
and etc_txn_not_already_used(t)):
if (
t['to'] == payeeAddress and t['amount'] >= amount and
not etc_txn_already_used(t)
):
return t
return None

Expand All @@ -512,15 +520,14 @@ def get_etc_txn_status(txnid, network='mainnet'):
return None


def sync_etc_payout(bounty):
t = search_for_etc_bounty_payout(bounty)
def sync_etc_payout(fulfillment):
t = search_for_etc_bounty_payout(fulfillment)
if t:
if not etc_txn_already_used(t):
bounty.payout_tx_id = t['hash']
bounty.save()
if get_etc_txn_status.get('has_mined'):
bounty.payout_confirmed = True
bounty.save()
fulfillment.payout_tx_id = t['hash']
if get_etc_txn_status(fulfillment.payout_tx_id).get('has_mined'):
fulfillment.payout_status = 'done'
fulfillment.save()


def get_bounty_id(issue_url, network):
Expand Down
169 changes: 104 additions & 65 deletions app/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
from .utils import (
apply_new_bounty_deadline, get_bounty, get_bounty_id, get_context, get_etc_txn_status, get_unrated_bounties_count,
get_web3, has_tx_mined, is_valid_eth_address, re_market_bounty, record_user_action_on_interest,
release_bounty_to_the_public, web3_process_bounty,
release_bounty_to_the_public, sync_etc_payout, web3_process_bounty,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -154,14 +154,15 @@ def org_perms(request):


@staff_member_required
def manual_sync_etc_payout(request, bounty_id):
b = Bounty.objects.get(id=bounty_id)
if b.payout_confirmed:
def manual_sync_etc_payout(request, fulfillment_id):
fulfillment = BountyFulfillment.objects.get(id=fulfillment_id)
if fulfillment.payout_confirmed:
return JsonResponse(
{'error': _('Bounty payout already confirmed'),
'success': False},
status=401)
sync_etc_payout(b)

sync_etc_payout(fulfillment)
return JsonResponse({'success': True}, status=200)


Expand Down Expand Up @@ -3064,66 +3065,6 @@ def sync_web3(request):
return JsonResponse(result, status=result['status'])


# @require_POST
# @csrf_exempt
# @ratelimit(key='ip', rate='5/s', method=ratelimit.UNSAFE, block=True)
@staff_member_required
def sync_etc(request):
"""Sync up ETC chain to find transction status.

Returns:
JsonResponse: The JSON response following the web3 sync.

"""

response = {
'status': '400',
'message': 'bad request'
}

# TODO: make into POST
txnid = request.GET.get('txnid', None)
bounty_id = request.GET.get('id', None)
network = request.GET.get('network', 'mainnet')

# TODO: REMOVE
txnid = '0x30060f38c0e9e255061d1daf079d3707c640bfb540e207dbc6fc0e6e6d52ecd1'
bounty_id = 1

if not txnid:
response['message'] = 'error: transaction not provided'
elif not bounty_id:
response['message'] = 'error: issue url not provided'
elif network != 'mainnet':
response['message'] = 'error: etc syncs only on mainnet'
else:
# TODO: CHECK IF BOUNTY EXSISTS
# bounty = Bounty.object.get(pk=bounty_id)
# if not bounty:
# response['message'] = f'error: bounty with key {bounty_id} does not exist'
# else:
# print('bounty found') # wrap whole section below within else

transaction = get_etc_txn_status(txnid, network)
if not transaction:
logging.error('blockscout failed')
response = {
'status': 500,
'message': 'blockscout API call failed'
}
else:
response = {
'status': 200,
'message': 'success',
'id': bounty_id,
'bounty_url': '<bounty_url>',
'blockNumber': transaction['blockNumber'],
'confirmations': transaction['confirmations'],
'is_mined': transaction['has_mined']
}

return JsonResponse(response, status=response['status'])

# LEGAL
@xframe_options_exempt
def terms(request):
Expand Down Expand Up @@ -4959,3 +4900,101 @@ def fulfill_bounty_v1(request):
}

return JsonResponse(response)


@csrf_exempt
@require_POST
@staff_member_required
def payout_bounty_v1(request, fulfillment_id):
'''
ETC-TODO
- wire in email (invite + successful payout)
- invoke blockscout to check status
- add new bounty_state : pending verification
- handle multiple payouts

{
amount: <integer>,
bounty_owner_address : <char>,
close_bounty: <bool>,
token_name : <char>
}
'''
response = {
'status': 400,
'message': 'error: Bad Request. Unable to payout bounty'
}

user = request.user if request.user.is_authenticated else None

if not user:
response['message'] = 'error: user needs to be authenticated to fulfill bounty'
return JsonResponse(response)

profile = request.user.profile if hasattr(request.user, 'profile') else None

if not profile:
response['message'] = 'error: no matching profile found'
return JsonResponse(response)

if not request.method == 'POST':
response['message'] = 'error: fulfill bounty is a POST operation'
return JsonResponse(response)

if not fulfillment_id:
response['message'] = 'error: missing parameter fulfillment_id'
return JsonResponse(response)

try:
fulfillment = BountyFulfillment.objects.get(fulfillment_id)
bounty = fulfillment.bounty
except BountyFulfillment.DoesNotExist:
response['message'] = 'error: bounty fulfillment not found'
return JsonResponse(response)


if bounty.bounty_state in ['cancelled', 'done']:
response['message'] = 'error: bounty in ' + bounty.bounty_state + ' state cannot be paid out'
return JsonResponse(response)

is_funder = bounty.is_funder(user.username.lower()) if user else False

if not is_funder:
response['message'] = 'error: payout is bounty funder operation'
return JsonResponse(response)

if not bounty.bounty_owner_address:
bounty_owner_address = request.POST.get('bounty_owner_address')
if not bounty_owner_address:
response['message'] = 'error: missing parameter bounty_owner_address'
return JsonResponse(response)

bounty.bounty_owner_address = bounty_owner_address


amount = request.POST.get('amount')
if not amount:
response['message'] = 'error: missing parameter amount'
return JsonResponse(response)

token_name = request.POST.get('token_name')
if not token_name:
response['message'] = 'error: missing parameter token_name'
return JsonResponse(response)

fulfillment.payout_amount = amount
fulfillment.token_name = token_name
fulfillment.save()

if request.POST.get('close_bounty') == True:
bounty.bounty_state = 'done'

sync_etc_payout(fulfillment)

response = {
'status': 204,
'message': 'bounty payment recorded. verification pending',
'fulfillment_id': fulfillment_id
}

return JsonResponse(response)