diff --git a/app/assets/v2/images/chains/rsk.svg b/app/assets/v2/images/chains/rsk.svg new file mode 100644 index 00000000000..7e3cb0db28c --- /dev/null +++ b/app/assets/v2/images/chains/rsk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/v2/js/pages/bounty_detail/binance_extension.js b/app/assets/v2/js/pages/bounty_detail/binance_extension.js index 90896a7117b..80d77cc8b37 100644 --- a/app/assets/v2/js/pages/bounty_detail/binance_extension.js +++ b/app/assets/v2/js/pages/bounty_detail/binance_extension.js @@ -3,7 +3,7 @@ const payWithBinanceExtension = (fulfillment_id, to_address, vm, modal) => { const amount = vm.fulfillment_context.amount; const token_name = vm.bounty.token_name; const from_address = vm.bounty.bounty_owner_address; - + binance_utils.transferViaExtension( amount * 10 ** vm.decimals, to_address, @@ -14,13 +14,13 @@ const payWithBinanceExtension = (fulfillment_id, to_address, vm, modal) => { }).catch(err => { callback(err); }); - + function callback(error, from_address, txn) { if (error) { _alert({ message: gettext('Unable to payout bounty due to: ' + error) }, 'error'); console.log(error); } else { - + const payload = { payout_type: 'binance_ext', tenant: 'BINANCE', @@ -29,22 +29,22 @@ const payWithBinanceExtension = (fulfillment_id, to_address, vm, modal) => { funder_address: from_address, payout_tx_id: txn }; - + modal.closeModal(); const apiUrlBounty = `/api/v1/bounty/payout/${fulfillment_id}`; - + fetchData(apiUrlBounty, 'POST', payload).then(response => { if (200 <= response.status && response.status <= 204) { console.log('success', response); - + vm.fetchBounty(); _alert('Payment Successful'); - + } else { _alert('Unable to make payout bounty. Please try again later', 'error'); console.error(`error: bounty payment failed with status: ${response.status} and message: ${response.message}`); } - }).catch(function(error) { + }).catch(function (error) { _alert('Unable to make payout bounty. Please try again later', 'error'); console.log(error); }); diff --git a/app/assets/v2/js/pages/bounty_detail/rsk_extension.js b/app/assets/v2/js/pages/bounty_detail/rsk_extension.js new file mode 100644 index 00000000000..548413f2f03 --- /dev/null +++ b/app/assets/v2/js/pages/bounty_detail/rsk_extension.js @@ -0,0 +1,126 @@ +const payWithRSKExtension = async (fulfillment_id, to_address, vm, modal) => { + + const amount = vm.fulfillment_context.amount; + const token_name = vm.bounty.token_name; + + // 1. init rsk provider + // const rskHost = "https://public-node.testnet.rsk.co"; + const rskHost = "https://public-node.rsk.co"; + const rskClient = new Web3(); + rskClient.setProvider( + new rskClient.providers.HttpProvider(rskHost) + ); + + // Prompt user to unlock wallet if ethereum.selectedAddress is not present + if (!provider) { + try { + console.log(ethereum.selectedAddress); + } catch (e) { + modal.closeModal(); + _alert({ message: 'Please download or enable Nifty Wallet extension' }, 'error'); + return; + } + + if (!ethereum.selectedAddress) { + modal.closeModal(); + return onConnect().then(() => { + modal.openModal(); + }); + } + } + + // 2. construct + sign txn via nifty + let txArgs; + + if (token_name == 'R-BTC') { + + balanceInWei = await rskClient.eth.getBalance(ethereum.selectedAddress); + + rbtcBalance = rskClient.utils.fromWei(balanceInWei, 'ether'); + + if (Number(rbtcBalance) < amount) { + _alert({ message: `Insufficent balance in address ${ethereum.selectedAddress}` }, 'error'); + return; + } + + txArgs = { + to: to_address.toLowerCase(), + from: ethereum.selectedAddress, + value: rskClient.utils.toHex(rskClient.utils.toWei(String(amount))), + gasPrice: rskClient.utils.toHex(await rskClient.eth.getGasPrice()), + gas: rskClient.utils.toHex(318730), + gasLimit: rskClient.utils.toHex(318730) + }; + + } else { + + tokenContract = new rskClient.eth.Contract(token_abi, vm.bounty.token_address); + + balance = tokenContract.methods.balanceOf( + ethereum.selectedAddress).call({from: ethereum.selectedAddress}); + + amountInWei = amount * 1.0 * Math.pow(10, vm.decimals); + + if (Number(balance) < amountInWei) { + _alert({ message: `Insufficent balance in address ${ethereum.selectedAddress}` }, 'error'); + return; + } + + amountAsString = new rskClient.utils.BN(BigInt(amountInWei)).toString(); + data = tokenContract.methods.transfer(to_address.toLowerCase(), amountAsString).encodeABI(); + + txArgs = { + to: vm.bounty.token_address, + from: ethereum.selectedAddress, + gasPrice: rskClient.utils.toHex(await rskClient.eth.getGasPrice()), + gas: rskClient.utils.toHex(318730), + gasLimit: rskClient.utils.toHex(318730), + data: data + }; + } + + const txHash = await ethereum.request( + { + method: 'eth_sendTransaction', + params: [txArgs], + } + ); + + callback(null, ethereum.selectedAddress, txHash) + + function callback(error, from_address, txn) { + if (error) { + _alert({ message: gettext('Unable to payout bounty due to: ' + error) }, 'error'); + console.log(error); + } else { + + const payload = { + payout_type: 'rsk_ext', + tenant: 'RSK', + amount: amount, + token_name: token_name, + funder_address: from_address, + payout_tx_id: txn + }; + + modal.closeModal(); + const apiUrlBounty = `/api/v1/bounty/payout/${fulfillment_id}`; + + fetchData(apiUrlBounty, 'POST', payload).then(response => { + if (200 <= response.status && response.status <= 204) { + console.log('success', response); + + vm.fetchBounty(); + _alert('Payment Successful'); + + } else { + _alert('Unable to make payout bounty. Please try again later', 'error'); + console.error(`error: bounty payment failed with status: ${response.status} and message: ${response.message}`); + } + }).catch(function(error) { + _alert('Unable to make payout bounty. Please try again later', 'error'); + console.log(error); + }); + } + } +}; \ No newline at end of file diff --git a/app/assets/v2/js/pages/bounty_details2.js b/app/assets/v2/js/pages/bounty_details2.js index 5eb73563268..aca205a97c5 100644 --- a/app/assets/v2/js/pages/bounty_details2.js +++ b/app/assets/v2/js/pages/bounty_details2.js @@ -136,6 +136,12 @@ Vue.mixin({ url = `https://explorer.harmony.one/#/tx/${txn}`; break; + case 'R-BTC': + case 'RDOC': + case 'DOC': + url = `https://explorer.rsk.co/tx/${txn}`; + break; + default: url = `https://etherscan.io/tx/${txn}`; @@ -184,6 +190,12 @@ Vue.mixin({ url = `https://explorer.harmony.one/#/address/${address}`; break; + case 'R-BTC': + case 'RDOC': + case 'DOC': + url = `https://explorer.rsk.co/address/${address}`; + break; + default: url = `https://etherscan.io/address/${address}`; } @@ -399,6 +411,12 @@ Vue.mixin({ tenant = 'HARMONY'; break; + case 'R-BTC': + case 'DOC': + case 'RDOC': + tenant = 'RSK'; + break; + default: tenant = 'ETH'; } @@ -481,6 +499,10 @@ Vue.mixin({ case 'harmony_ext': payWithHarmonyExtension(fulfillment_id, fulfiller_address, vm, modal); break; + + case 'rsk_ext': + payWithRSKExtension(fulfillment_id, fulfiller_address, vm, modal); + break; } }, closeBounty: function() { @@ -697,6 +719,7 @@ Vue.mixin({ case 'web3_modal': case 'polkadot_ext': + case 'rsk_ext': vm.fulfillment_context.active_step = 'payout_amount'; break; } diff --git a/app/assets/v2/js/pages/hackathon_new_bounty.js b/app/assets/v2/js/pages/hackathon_new_bounty.js index f08f3308a30..398d4998433 100644 --- a/app/assets/v2/js/pages/hackathon_new_bounty.js +++ b/app/assets/v2/js/pages/hackathon_new_bounty.js @@ -54,7 +54,6 @@ Vue.mixin({ vm.tokens = response; vm.form.token = vm.filterByChainId[0]; vm.getAmount(vm.form.token.symbol); - vm.injectProvider(vm.form.token.symbol); }).catch((err) => { console.log(err); @@ -88,38 +87,6 @@ Vue.mixin({ console.log(err); }); }, - injectProvider: function(token) { - let vm = this; - const chainId = vm.chainId; - - if (!token || !chainId) { - return; - } - - switch (chainId) { - case '58': { - let polkadot_endpoint; - - if (token == 'KSM') { - polkadot_endpoint = KUSAMA_ENDPOINT; - } else if (token == 'DOT') { - polkadot_endpoint = POLKADOT_ENDPOINT; - } - - polkadot_utils.connect(polkadot_endpoint).then(res =>{ - console.log(res); - polkadot_extension_dapp.web3Enable('gitcoin'); - }).catch(err => { - console.log(err); - }); - break; - } - - default: - break; - - } - }, calcValues: function(direction) { let vm = this; @@ -183,6 +150,10 @@ Vue.mixin({ // ethereum type = 'web3_modal'; break; + case '30': + // rsk + type = 'rsk_ext'; + break; case '58': // polkadot type = 'polkadot_ext'; diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index 4221fcdb10d..e7e6265bbd3 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -72,7 +72,6 @@ Vue.mixin({ vm.tokens = response; vm.form.token = vm.filterByChainId[0]; vm.getAmount(vm.form.token.symbol); - vm.injectProvider(vm.form.token.symbol); }).catch((err) => { console.log(err); @@ -105,39 +104,6 @@ Vue.mixin({ console.log(err); }); }, - injectProvider: function(token) { - let vm = this; - const chainId = vm.chainId; - - if (!token || !chainId) { - return; - } - - switch (chainId) { - case '59': - case '58': { - let polkadot_endpoint; - - if (token == 'KSM') { - polkadot_endpoint = KUSAMA_ENDPOINT; - } else if (token == 'DOT') { - polkadot_endpoint = POLKADOT_ENDPOINT; - } - - polkadot_utils.connect(polkadot_endpoint).then(res =>{ - console.log(res); - polkadot_extension_dapp.web3Enable('gitcoin'); - }).catch(err => { - console.log(err); - }); - break; - } - - - default: - break; - } - }, calcValues: function(direction) { let vm = this; @@ -223,6 +189,10 @@ Vue.mixin({ // ethereum type = 'web3_modal'; break; + case '30': + // rsk + type = 'rsk_ext'; + break; case '59': case '58': // 58 - polkadot, 59 - kusama diff --git a/app/dashboard/management/commands/sync_pending_fulfillments.py b/app/dashboard/management/commands/sync_pending_fulfillments.py index 45af912c93a..76e48a7628a 100644 --- a/app/dashboard/management/commands/sync_pending_fulfillments.py +++ b/app/dashboard/management/commands/sync_pending_fulfillments.py @@ -36,29 +36,12 @@ def handle(self, *args, **options): ) # Extensions - ext_payout_types= ['web3_modal', 'polkadot_ext', 'harmony_ext'] + ext_payout_types= ['web3_modal', 'polkadot_ext', 'harmony_ext', 'binance_ext', 'rsk_ext'] for ext_payout_type in ext_payout_types: ext_pending_fulfillments = pending_fulfillments.filter(payout_type=ext_payout_type) for fulfillment in ext_pending_fulfillments.all(): sync_payout(fulfillment) - # polkadot extension - polkadot_pending_fulfillments = pending_fulfillments.filter(payout_type='polkadot_ext') - if polkadot_pending_fulfillments: - for fulfillment in polkadot_pending_fulfillments.all(): - sync_payout(fulfillment) - - # binance extension - binance_pending_fulfillments = pending_fulfillments.filter(payout_type='binance_ext') - if binance_pending_fulfillments: - for fulfillment in binance_pending_fulfillments.all(): - sync_payout(fulfillment) - - # harmony extension - harmony_pending_fulfillments = pending_fulfillments.filter(payout_type='harmony_ext') - if harmony_pending_fulfillments: - for fulfillment in harmony_pending_fulfillments.all(): - sync_payout(fulfillment) # QR qr_pending_fulfillments = pending_fulfillments.filter(payout_type='qr') diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 0364623453c..39d37c63867 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -294,6 +294,7 @@ class Bounty(SuperModel): ('polkadot_ext', 'Polkadot Ext'), ('binance_ext', 'Binance Ext'), ('harmony_ext', 'Harmony Ext'), + ('rsk_ext', 'RSK Ext'), ('fiat', 'Fiat'), ('manual', 'Manual') ) @@ -1410,6 +1411,7 @@ class BountyFulfillment(SuperModel): ('polkadot_ext', 'polkadot_ext'), ('binance_ext', 'binance_ext'), ('harmony_ext', 'harmony_ext'), + ('rsk_ext', 'rsk_ext'), ('manual', 'manual') ] @@ -1424,6 +1426,7 @@ class BountyFulfillment(SuperModel): ('BINANCE', 'BINANCE'), ('HARMONY', 'HARMONY'), ('FILECOIN', 'FILECOIN'), + ('RSK', 'RSK'), ('OTHERS', 'OTHERS') ] @@ -1462,7 +1465,7 @@ class BountyFulfillment(SuperModel): token_name = models.CharField(max_length=10, blank=True, help_text="token/currency in which the payout is done") payout_tx_id = models.CharField(default="0x0", max_length=255, blank=True, help_text="transaction id") payout_status = models.CharField(max_length=10, choices=PAYOUT_STATUS, blank=True, help_text="payment status") - payout_amount = models.DecimalField(null=True, blank=True, decimal_places=4, max_digits=50, help_text="amount being paid out by funder") + payout_amount = models.DecimalField(null=True, blank=True, decimal_places=6, max_digits=50, help_text="amount being paid out by funder") def __str__(self): """Define the string representation of BountyFulfillment. diff --git a/app/dashboard/sync/helpers.py b/app/dashboard/sync/helpers.py index 2fc623db1a7..5e91c223900 100644 --- a/app/dashboard/sync/helpers.py +++ b/app/dashboard/sync/helpers.py @@ -26,7 +26,7 @@ def record_payout_activity(fulfillment): kwargs['profile'] = fulfillment.funder_profile kwargs['metadata']['from'] = fulfillment.funder_profile.handle kwargs['metadata']['to'] = fulfillment.profile.handle - kwargs['metadata']['payout_amount'] = fulfillment.payout_amount + kwargs['metadata']['payout_amount'] = str(fulfillment.payout_amount) kwargs['metadata']['token_name'] = fulfillment.token_name try: diff --git a/app/dashboard/sync/rsk.py b/app/dashboard/sync/rsk.py new file mode 100644 index 00000000000..60f8a4f4c5c --- /dev/null +++ b/app/dashboard/sync/rsk.py @@ -0,0 +1,97 @@ +import json + +from django.conf import settings +from django.utils import timezone + +import requests +from dashboard.sync.helpers import record_payout_activity, txn_already_used + + +def find_txn_on_rsk_explorer(fulfillment): + token_name = fulfillment.token_name + + funderAddress = fulfillment.bounty.bounty_owner_address + amount = fulfillment.payout_amount + payeeAddress = fulfillment.fulfiller_address + + if token_name not in ['R-BTC', 'RDOC', 'DOC']: + return None + + url = f'https://blockscout.com/rsk/mainnet/api?module=account&action=txlist&address={funderAddress}' + + response = requests.get(url).json() + + if response['message'] and response['result']: + for txn in response['result']: + to_address_match = txn['to'] == payeeAddress.lower() if token_name == 'R-BTC' else True + + if ( + txn['from'] == funderAddress.lower() and + to_address_match and + # float(txn['value']) == float(amount * 10 ** 18) and + not txn_already_used(txn['hash'], token_name) + ): + return txn + return None + + +def get_rsk_txn_status(fulfillment): + + txnid = fulfillment.payout_tx_id + token_name = fulfillment.token_name + funderAddress = fulfillment.bounty.bounty_owner_address + payeeAddress = fulfillment.fulfiller_address + + amount = fulfillment.payout_amount + + + if token_name not in ['R-BTC', 'RDOC', 'DOC']: + return None + + if not txnid or txnid == "0x0": + return None + + url = f'https://blockscout.com/rsk/mainnet/api?module=transaction&action=gettxinfo&txhash={txnid}' + + response = requests.get(url).json() + + if response['status'] and response['result']: + txn = response['result'] + + to_address_match = txn['to'] == payeeAddress.lower() if token_name == 'R-BTC' else True + + if ( + txn['from'] == funderAddress.lower() and + to_address_match and + # float(txn['value']) == float(amount * 10 ** 18) and + not txn_already_used(txn['hash'], token_name) and + int(txn['confirmations']) > 0 + ): + if txn['success']: + return 'success' + return 'expired' + + + return None + + +def sync_rsk_payout(fulfillment): + if not fulfillment.payout_tx_id or fulfillment.payout_tx_id == "0x0": + txn = find_txn_on_rsk_explorer(fulfillment) + if txn: + fulfillment.payout_tx_id = txn['hash'] + fulfillment.save() + + if fulfillment.payout_tx_id and fulfillment.payout_tx_id != "0x0": + txn_status = get_rsk_txn_status(fulfillment) + + if txn_status == 'success': + fulfillment.payout_status = 'done' + fulfillment.accepted_on = timezone.now() + fulfillment.accepted = True + record_payout_activity(fulfillment) + + elif txn_status == 'expired': + fulfillment.payout_status = 'expired' + + fulfillment.save() diff --git a/app/dashboard/templates/bounty/details2.html b/app/dashboard/templates/bounty/details2.html index fde95a4ee01..146b0dd3a44 100644 --- a/app/dashboard/templates/bounty/details2.html +++ b/app/dashboard/templates/bounty/details2.html @@ -435,7 +435,7 @@
{% trans "SUBMISSIONS" %}
-
+

Payout

@@ -1121,6 +1121,10 @@

{{ noscript.keywords }}

+ {% elif web3_type == 'rsk_ext' %} + + + {% elif web3_type == 'fiat' %} {% if PYPL_CLIENT_ID %} diff --git a/app/dashboard/templates/bounty/new_bounty.html b/app/dashboard/templates/bounty/new_bounty.html index f9f35241700..3c8cd5fe1e4 100644 --- a/app/dashboard/templates/bounty/new_bounty.html +++ b/app/dashboard/templates/bounty/new_bounty.html @@ -133,6 +133,10 @@

Fund Issue

{% if is_staff %} + + @@ -732,10 +736,6 @@
Total
- - - - diff --git a/app/dashboard/templates/dashboard/hackathon/new_bounty.html b/app/dashboard/templates/dashboard/hackathon/new_bounty.html index b878b0e2925..fa5cf4fda98 100644 --- a/app/dashboard/templates/dashboard/hackathon/new_bounty.html +++ b/app/dashboard/templates/dashboard/hackathon/new_bounty.html @@ -192,6 +192,12 @@

Fund Prize

Celo + {% if is_staff %} + + {% endif %} + @@ -513,10 +519,6 @@
Total
- - - -