diff --git a/app/assets/v2/images/chains/binance.svg b/app/assets/v2/images/chains/binance.svg new file mode 100644 index 00000000000..e86670aa4c5 --- /dev/null +++ b/app/assets/v2/images/chains/binance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/v2/js/lib/binance/utils.js b/app/assets/v2/js/lib/binance/utils.js new file mode 100644 index 00000000000..c0a8e0d5902 --- /dev/null +++ b/app/assets/v2/js/lib/binance/utils.js @@ -0,0 +1,126 @@ +var binance_utils = {}; + +binance_utils.getChainVerbose = chainId => { + switch (chainId) { + case 'Binance-Chain-Tigris': + return { name: 'Binance Chain Network', addressType: 'bbc-mainnet' }; + case 'Binance-Chain-Ganges': + return { name: 'Binance Chain Test Network', addressType: 'bbc-testnet' }; + case '0x38': + return { name: 'Binance Smart Chain Network', addressType: 'eth' }; + case '0x61': + return { name: 'Binance Smart Chain Test Network', addressType: 'eth' }; + } +} + + +/** + * Returns wallet's balance on the connected binance network + * @param {String} address + */ +binance_utils.getAddressBalance = async address => { + const isConnected = await BinanceChain.isConnected(); + + if (!isConnected || !address) + return; + + data = { + method: 'eth_getBalance', + params: [address, 'latest'] + }; + + const result = await BinanceChain.request(data); + + // convert hex balance to integer and account for decimal points + const bnbBalance = BigInt(result).toString(10) * 10 ** -18; + + return Promise.resolve(bnbBalance.toFixed(4)); +}; + + +/** + * Get accounts connected in extension + */ +binance_utils.getExtensionConnectedAccounts = async () => { + const isConnected = await BinanceChain.isConnected(); + + if (!isConnected) + return; + + const accounts = await BinanceChain.requestAccounts(); + + return Promise.resolve(accounts); +}; + + +/** + * Sign and transfer token to another address via extension and returns txn hash + * @param {Number} amount + * @param {String} to_address + * @param {String} from_address : optional, if not passed takes account first account from getExtensionConnectedAccounts + */ +binance_utils.transferViaExtension = async (amount, to_address, from_address) => { + + return new Promise(async(resolve, reject) => { + + const isConnected = await BinanceChain.isConnected(); + + if (!isConnected) { + reject(`transferViaExtension: binance hasn't connected to the network ${binance_utils.getChainVerbose(BinanceChain.chainId).name}`); + } else if (!amount) { + reject('transferViaExtension: missing param amount'); + } else if (!to_address) { + reject('transferViaExtension: missing param to_address'); + } + + const chainVerbose = binance_utils.getChainVerbose(BinanceChain.chainId); + + if (!from_address) { + const accounts = await binance_utils.getExtensionConnectedAccounts(); + from_address = accounts && accounts[0]['addresses'].find(address => address.type === chainVerbose.addressType).address; + } + + if (!from_address) { + reject('transferViaExtension: missing param from_address'); + } + + const account_balance = await binance_utils.getAddressBalance(from_address); + + if (Number(account_balance) < amount) { + reject(`transferViaExtension: insufficent balance in address ${from_address}`); + } + + if (chainVerbose.addressType === 'eth') { + const params = [ + { + from: from_address, + to: to_address, + value: '0x' + amount.toString(16) // convert amount to hex + }, + ]; + + BinanceChain + .request({ + method: 'eth_sendTransaction', + params + }) + .then(txHash => { + resolve(txHash); + }) + .catch(error => { + reject('transferViaExtension: something went wrong' + error); + }); + } + }); +}; + + +/* EVENTS */ +BinanceChain.on('connect', info => { + console.log(`connected to ${binance_utils.getChainVerbose(info.chainId).name}!`); +}); + +BinanceChain.on('chainChanged', chainId => { + console.log(`connected to ${binance_utils.getChainVerbose(chainId).name}!`); + window.location.reload(); // reload page when chain changes +}); diff --git a/app/assets/v2/js/pages/bounty_detail/binance_extension.js b/app/assets/v2/js/pages/bounty_detail/binance_extension.js new file mode 100644 index 00000000000..ef026305be3 --- /dev/null +++ b/app/assets/v2/js/pages/bounty_detail/binance_extension.js @@ -0,0 +1,52 @@ +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, + from_address + ).then(txn => { + callback(null, from_address, txn); + }).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', + 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 f97c4d5a381..03be9e3f506 100644 --- a/app/assets/v2/js/pages/bounty_details2.js +++ b/app/assets/v2/js/pages/bounty_details2.js @@ -78,6 +78,10 @@ Vue.mixin({ url = `https://filscan.io/#/tipset/message-detail?cid=${txn}`; break; + case 'BNB': + url = `https://bscscan.com/tx/${txn}`; + break; + case 'ONE': url = `https://explorer.harmony.one/#/tx/${txn}`; break; @@ -121,6 +125,10 @@ Vue.mixin({ url = `https://filscan.io/#/tipset/address-detail?address=${address}`; break; + case 'BNB': + url = `https://bscscan.com/address/${address}`; + break; + case 'ONE': url = `https://explorer.harmony.one/#/address/${address}`; break; @@ -331,6 +339,10 @@ Vue.mixin({ tenant = 'FILECOIN'; break; + case 'BNB': + tenant = 'BINANCE'; + break; + case 'ONE': tenant = 'HARMONY'; break; @@ -410,11 +422,14 @@ Vue.mixin({ payWithPolkadotExtension(fulfillment_id, fulfiller_address, vm, modal); break; + case 'binance_ext': + payWithBinanceExtension(fulfillment_id, fulfiller_address, vm, modal); + break; + case 'harmony_ext': payWithHarmonyExtension(fulfillment_id, fulfiller_address, vm, modal); break; } - }, closeBounty: function() { @@ -622,6 +637,11 @@ Vue.mixin({ break; } + case 'binance_ext': { + vm.fulfillment_context.active_step = 'payout_amount'; + break; + } + case 'harmony_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 7b120399e68..263a2ba84b6 100644 --- a/app/assets/v2/js/pages/hackathon_new_bounty.js +++ b/app/assets/v2/js/pages/hackathon_new_bounty.js @@ -178,6 +178,10 @@ Vue.mixin({ // polkadot type = 'polkadot_ext'; break; + case '56': + // binance + type = 'binance_ext'; + break; case '1000': // harmony type = 'harmony_ext'; diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index 8a1e9aa6133..de876bee74f 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -118,6 +118,7 @@ Vue.mixin({ break; } + default: break; } @@ -211,6 +212,10 @@ Vue.mixin({ // polkadot type = 'polkadot_ext'; break; + case '56': + // binance + type = 'binance_ext'; + break; case '1000': // harmony type = 'harmony_ext'; diff --git a/app/dashboard/management/commands/sync_pending_fulfillments.py b/app/dashboard/management/commands/sync_pending_fulfillments.py index 2f5bcb81ee7..797a1b6fb8b 100644 --- a/app/dashboard/management/commands/sync_pending_fulfillments.py +++ b/app/dashboard/management/commands/sync_pending_fulfillments.py @@ -47,13 +47,18 @@ def handle(self, *args, **options): 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') if qr_pending_fulfillments: diff --git a/app/dashboard/migrations/0157_auto_20201021_2319.py b/app/dashboard/migrations/0157_auto_20201021_2319.py new file mode 100644 index 00000000000..f865ef0d319 --- /dev/null +++ b/app/dashboard/migrations/0157_auto_20201021_2319.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2020-10-21 23:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0156_auto_20201015_1742'), + ] + + operations = [ + migrations.AlterField( + model_name='bounty', + name='web3_type', + field=models.CharField(choices=[('legacy_gitcoin', 'Legacy Bounty'), ('bounties_network', 'Bounties Network'), ('qr', 'QR Code'), ('web3_modal', 'Web3 Modal'), ('polkadot_ext', 'Polkadot Ext'), ('binance_ext', 'Binance Ext'), ('fiat', 'Fiat'), ('manual', 'Manual')], default='bounties_network', max_length=50), + ), + migrations.AlterField( + model_name='bountyfulfillment', + name='payout_type', + field=models.CharField(blank=True, choices=[('bounties_network', 'bounties_network'), ('qr', 'qr'), ('fiat', 'fiat'), ('web3_modal', 'web3_modal'), ('polkadot_ext', 'polkadot_ext'), ('binance_ext', 'binance_ext'), ('manual', 'manual')], help_text='payment type used to make the payment', max_length=20, null=True), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 404f466d0c2..6bbd777938a 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -292,6 +292,7 @@ class Bounty(SuperModel): ('qr', 'QR Code'), ('web3_modal', 'Web3 Modal'), ('polkadot_ext', 'Polkadot Ext'), + ('binance_ext', 'Binance Ext'), ('harmony_ext', 'Harmony Ext'), ('fiat', 'Fiat'), ('manual', 'Manual') @@ -1406,6 +1407,7 @@ class BountyFulfillment(SuperModel): ('fiat', 'fiat'), ('web3_modal', 'web3_modal'), ('polkadot_ext', 'polkadot_ext'), + ('binance_ext', 'binance_ext'), ('harmony_ext', 'harmony_ext'), ('manual', 'manual') ] @@ -1418,6 +1420,7 @@ class BountyFulfillment(SuperModel): ('CELO', 'CELO'), ('PYPL', 'PYPL'), ('POLKADOT', 'POLKADOT'), + ('BINANCE', 'BINANCE'), ('HARMONY', 'HARMONY'), ('FILECOIN', 'FILECOIN'), ('OTHERS', 'OTHERS') diff --git a/app/dashboard/sync/binance.py b/app/dashboard/sync/binance.py new file mode 100644 index 00000000000..1992f33a0b8 --- /dev/null +++ b/app/dashboard/sync/binance.py @@ -0,0 +1,73 @@ +import logging + +from django.utils import timezone + +import requests +from dashboard.sync.helpers import record_payout_activity + +logger = logging.getLogger(__name__) + + +def get_binance_txn_status(fulfillment): + txnid = fulfillment.payout_tx_id + network = fulfillment.bounty.network if fulfillment.bounty.network else None + + if not txnid: + return None + + response = { 'status': 'pending' } + + try: + if network == 'mainnet': + binance_url = f'https://bsc-dataseed.binance.org' + else: + binance_url = f'https://data-seed-prebsc-1-s1.binance.org:8545' + + data = { + 'id': 0, + 'jsonrpc': '2.0', + 'method': 'eth_getTransactionReceipt', + 'params': [ txnid ] + } + + headers = { + 'Host': 'gitcoin.co' + } + + binance_response = requests.post(binance_url, json=data, headers=headers).json() + + result = binance_response['result'] + + response = { 'status': 'pending' } + + if result: + tx_status = int(result.get('status'), 16) # convert hex to decimal + + if tx_status == '1': + response = { 'status': 'done' } + elif tx_status == '0': + response = { 'status': 'expired' } + + except Exception as e: + logger.error(f'error: get_binance_txn_status - {e}') + + finally: + return response + + +def sync_binance_payout(fulfillment): + if fulfillment.payout_tx_id: + txn_status = get_binance_txn_status(fulfillment) + + if txn_status: + status_description = txn_status.get('status') + + if status_description == 'done': + fulfillment.payout_status = 'done' + fulfillment.accepted_on = timezone.now() + fulfillment.accepted = True + fulfillment.save() + record_payout_activity(fulfillment) + elif status_description == 'expired': + fulfillment.payout_status = 'expired' + fulfillment.save() diff --git a/app/dashboard/sync/celo.py b/app/dashboard/sync/celo.py index 060c6a2a2f7..8ec708839b5 100644 --- a/app/dashboard/sync/celo.py +++ b/app/dashboard/sync/celo.py @@ -64,5 +64,4 @@ def sync_celo_payout(fulfillment): fulfillment.accepted_on = timezone.now() fulfillment.accepted = True record_payout_activity(fulfillment) - fulfillment.save() diff --git a/app/dashboard/sync/eth.py b/app/dashboard/sync/eth.py index f0968e628f1..0e62d5ced7f 100644 --- a/app/dashboard/sync/eth.py +++ b/app/dashboard/sync/eth.py @@ -71,15 +71,16 @@ def sync_eth_payout(fulfillment): fulfillment.payout_tx_id = replacement_payout_tx_id txn_status = get_eth_txn_status(fulfillment) if txn_status: - if txn_status.get('status') == 'done': + status_description = txn_status.get('status') + if status_description == 'done': fulfillment.payout_status = 'done' fulfillment.accepted_on = timezone.now() fulfillment.accepted = True + fulfillment.save() record_payout_activity(fulfillment) - elif txn_status.get('status') == 'expired': + elif status_description == 'expired': fulfillment.payout_status = 'expired' - - fulfillment.save() + fulfillment.save() def getReplacedTX(tx): diff --git a/app/dashboard/sync/filecoin.py b/app/dashboard/sync/filecoin.py index f45084b6cef..a5e52c163f1 100644 --- a/app/dashboard/sync/filecoin.py +++ b/app/dashboard/sync/filecoin.py @@ -94,12 +94,12 @@ def sync_filecoin_payout(fulfillment): fulfillment.payout_status = 'done' fulfillment.accepted_on = timezone.now() fulfillment.accepted = True + fulfillment.save() record_payout_activity(fulfillment) elif txn_status == 'expired': fulfillment.payout_status = 'expired' - - fulfillment.save() + fulfillment.save() def isValidTxn(fulfillment, txn): diff --git a/app/dashboard/sync/polkadot.py b/app/dashboard/sync/polkadot.py index adf064f7be3..66f756388f3 100644 --- a/app/dashboard/sync/polkadot.py +++ b/app/dashboard/sync/polkadot.py @@ -13,10 +13,6 @@ def get_polkadot_txn_status(txnid, token_name): if not txnid: return None - response = { - 'status': 'pending' - } - try: response = { 'status': 'pending' } @@ -54,12 +50,13 @@ def sync_polkadot_payout(fulfillment): if fulfillment.payout_tx_id: txn_status = get_polkadot_txn_status(fulfillment.payout_tx_id, fulfillment.token_name) if txn_status: - if txn_status.get('status') == 'done': + status_description = txn_status.get('status') + if status_description == 'done': fulfillment.payout_status = 'done' fulfillment.accepted_on = timezone.now() fulfillment.accepted = True + fulfillment.save() record_payout_activity(fulfillment) - elif txn_status.get('status') == 'expired': + elif status_description == 'expired': fulfillment.payout_status = 'expired' - - fulfillment.save() + fulfillment.save() diff --git a/app/dashboard/sync/zil.py b/app/dashboard/sync/zil.py index 8e05efcd64d..bb041f8bc8c 100644 --- a/app/dashboard/sync/zil.py +++ b/app/dashboard/sync/zil.py @@ -70,5 +70,4 @@ def sync_zil_payout(fulfillment): fulfillment.accepted_on = timezone.now() fulfillment.accepted = True fulfillment.save() - record_payout_activity(fulfillment) diff --git a/app/dashboard/templates/bounty/details2.html b/app/dashboard/templates/bounty/details2.html index f93f2c69741..320c7823750 100644 --- a/app/dashboard/templates/bounty/details2.html +++ b/app/dashboard/templates/bounty/details2.html @@ -433,7 +433,7 @@
{% trans "SUBMISSIONS" %}
-
+

Payout

@@ -1104,6 +1104,11 @@

{{ noscript.keywords }}

+ {% elif web3_type == 'binance_ext' %} + + + + {% elif web3_type == 'harmony_ext' %} diff --git a/app/dashboard/templates/bounty/new_bounty.html b/app/dashboard/templates/bounty/new_bounty.html index b946d9cb4c1..b8e411aec99 100644 --- a/app/dashboard/templates/bounty/new_bounty.html +++ b/app/dashboard/templates/bounty/new_bounty.html @@ -97,15 +97,19 @@

Fund Issue

- + {% if is_staff %} - + + {% endif %} -