From c4e1aa87665e072e02b5a01932cd4e695836fb10 Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Mon, 21 Jun 2021 17:38:04 +0530 Subject: [PATCH 1/5] feat: algorand grants integration --- app/assets/v2/js/cart.js | 14 ++ app/assets/v2/js/grants/_detail-component.js | 1 + app/assets/v2/js/grants/_new.js | 4 + .../v2/js/grants/cart/algorand_extension.js | 210 ++++++++++++++++++ app/assets/v2/js/grants/funding.js | 3 + .../pages/bounty_detail/algorand_extension.js | 1 - app/assets/v2/js/pages/bounty_details2.js | 2 +- app/grants/admin.py | 2 +- .../commands/sync_pending_contributions.py | 2 +- app/grants/models.py | 17 +- app/grants/sync/algorand.py | 106 +++++++++ app/grants/templates/grants/_new.html | 22 ++ .../templates/grants/bulk_add_to_cart.html | 1 + app/grants/templates/grants/cart-vue.html | 3 + .../grants/cart/extension/algorand.html | 117 ++++++++++ .../templates/grants/components/card.html | 1 + .../grants/detail/template-grant-details.html | 13 ++ app/grants/utils.py | 5 +- app/grants/views.py | 13 +- 19 files changed, 527 insertions(+), 10 deletions(-) create mode 100644 app/assets/v2/js/grants/cart/algorand_extension.js create mode 100644 app/grants/sync/algorand.py create mode 100644 app/grants/templates/grants/cart/extension/algorand.html diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index f1dab840121..85e2cea4495 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -90,6 +90,9 @@ Vue.component('grants-cart', { ], 'RSK': [ `${static_url}v2/js/grants/cart/rsk_extension.js` + ], + 'ALGORAND': [ + `${static_url}v2/js/grants/cart/algorand_extension.js` ] } }; @@ -369,6 +372,11 @@ Vue.component('grants-cart', { isBinanceExtInstalled() { return window.BinanceChain || false; }, + + isAlgorandExtInstalled() { + return window.AlgoSigner || false; + }, + isRskExtInstalled() { const rskHost = 'https://public-node.rsk.co'; const rskClient = new Web3(); @@ -468,6 +476,9 @@ Vue.component('grants-cart', { case 'RSK': vm.chainId = '30'; break; + case 'ALGORAND': + vm.chainId = '1001'; + break; } }, confirmQRPayment: function(e, grant) { @@ -543,6 +554,9 @@ Vue.component('grants-cart', { let vm = this; switch (tenant) { + case 'ALGORAND': + contributeWithAlgorandExtension(grant, vm); + break; case 'RSK': contributeWithRskExtension(grant, vm); break; diff --git a/app/assets/v2/js/grants/_detail-component.js b/app/assets/v2/js/grants/_detail-component.js index c0c8eef2736..11735236a4b 100644 --- a/app/assets/v2/js/grants/_detail-component.js +++ b/app/assets/v2/js/grants/_detail-component.js @@ -82,6 +82,7 @@ Vue.mixin({ 'polkadot_payout_address': vm.grant.polkadot_payout_address, 'kusama_payout_address': vm.grant.kusama_payout_address, 'rsk_payout_address': vm.grant.rsk_payout_address, + 'algorand_payout_address': vm.grant.algorand_payout_address, 'region': vm.grant.region?.name || undefined, 'has_external_funding': vm.grant.has_external_funding }; diff --git a/app/assets/v2/js/grants/_new.js b/app/assets/v2/js/grants/_new.js index 65ac9c55d57..ae19d88c9f5 100644 --- a/app/assets/v2/js/grants/_new.js +++ b/app/assets/v2/js/grants/_new.js @@ -127,6 +127,8 @@ Vue.mixin({ vm.$set(vm.errors, 'kusama_payout_address', 'Please enter Kusama address'); } else if (vm.chainId == 'rsk' && !vm.form.rsk_payout_address) { vm.$set(vm.errors, 'rsk_payout_address', 'Please enter RSK address'); + } else if (vm.chainId == 'algorand' && !vm.form.algorand_payout_address) { + vm.$set(vm.errors, 'algorand_payout_address', 'Please enter Algorand address'); } if (!vm.form.grant_type) { @@ -180,6 +182,7 @@ Vue.mixin({ 'polkadot_payout_address': form.polkadot_payout_address, 'kusama_payout_address': form.kusama_payout_address, 'rsk_payout_address': form.rsk_payout_address, + 'algorand_payout_address': form.algorand_payout_address, 'grant_type': form.grant_type, 'categories[]': form.grant_categories, 'network': form.network, @@ -363,6 +366,7 @@ if (document.getElementById('gc-new-grant')) { polkadot_payout_address: '', kusama_payout_address: '', rsk_payout_address: '', + algorand_payout_address: '', grant_type: '', grant_categories: [], network: 'mainnet' diff --git a/app/assets/v2/js/grants/cart/algorand_extension.js b/app/assets/v2/js/grants/cart/algorand_extension.js new file mode 100644 index 00000000000..e64a952a1ec --- /dev/null +++ b/app/assets/v2/js/grants/cart/algorand_extension.js @@ -0,0 +1,210 @@ +const contributeWithAlgorandExtension = async(grant, vm, modal) => { + const token_name = grant.grant_donation_currency; + const amount = grant.grant_donation_amount; + const to_address = grant.rsk_payout_address; + const token = vm.getTokenByName(token_name); + + // TODO: FIGURE OUT from_address + + // const NETWORK = 'TestNet'; + const NETWORK = 'MainNet'; + + // 1. check if AlgoSigner is available + if (!AlgoSigner) { + _alert({ message: 'Please download or enable AlgoSigner extension' }, 'danger'); + modal.closeModal(); + return; + } + + try { + AlgoSigner.connect().then(async() => { + // step2: get connected accounts + const accounts = await AlgoSigner.accounts({ ledger: NETWORK }); + + let is_account_present = false; + + accounts.map(account=> { + if (account.address == from_address) + is_account_present = true; + }); + + if (!is_account_present) { + _alert({ message: `Unable to access address ${from_address} in wallet` }, 'danger'); + modal.closeModal(); + return; + } + + // step3: check if enough balance is present + const balance = await AlgoSigner.algod({ + ledger: NETWORK, + path: `/v2/accounts/${from_address}` + }); + + if ( + token_name == 'ALGO' && + Number(balance.amount) <= amount * 10 ** token.decimals + ) { + // ALGO token + _alert({ message: `Insufficent balance in address ${from_address}` }, 'danger'); + modal.closeModal(); + return; + + } + // ALGO assets + let is_asset_present = false; + + if (balance.assets && balance.assets.length > 0) { + balance.assets.map(asset => { + if (asset['asset-id'] == asset_index) + is_asset_present = true; + }); + } + + if (is_asset_present) { + _alert({ message: `Asset ${token_name} is not present in ${from_address}` }, 'danger'); + modal.closeModal(); + return; + } + + let has_enough_asset_balance = false; + + balance.assets.map(asset => { + if (asset['asset-id'] == asset_index && asset['amount'] <= amount * 10 ** token.decimals) + has_enough_asset_balance = true; + }); + + if (has_enough_asset_balance) { + _alert({ message: `Insufficent balance in address ${from_address}` }, 'danger'); + modal.closeModal(); + return; + } + + + // step4: get txnParams + const txParams = await AlgoSigner.algod({ + ledger: NETWORK, + path: '/v2/transactions/params' + }); + + let txn; + // step5: sign transaction + + if (token_name == 'ALGO') { + // ALGO token + txn = { + from: from_address, + to: to_address, + fee: txParams['fee'], + type: 'pay', + amount: amount * 10 ** token.decimals, + firstRound: txParams['last-round'], + lastRound: txParams['last-round'] + 1000, + genesisID: txParams['genesis-id'], + genesisHash: txParams['genesis-hash'], + note: 'contributing to gitcoin grant' + }; + } else { + // ALGO assets + txn = { + from: from_address, + to: to_address, + assetIndex: Number(asset_index.addr), + note: 'contributing to gitcoin grant', + amount: amount * 10 ** token.decimals, + type: 'axfer', + fee: txParams['min-fee'], + firstRound: txParams['last-round'], + lastRound: txParams['last-round'] + 1000, + genesisID: txParams['genesis-id'], + genesisHash: txParams['genesis-hash'] + }; + } + + AlgoSigner.sign(txn).then(signedTx => { + + // step6: broadcast txn + AlgoSigner.send({ + ledger: NETWORK, + tx: signedTx.blob + }) + .then(tx => { + callback(null, from_address, tx.txId); + }) + .catch((e) => { + console.log(e); + _alert({ message: 'Unable to broadcast txn. Please try again' }, 'danger'); + modal.closeModal(); + return; + }); + + }).catch(e => { + console.log(e); + _alert({ message: 'Unable to sign txn. Please try again' }, 'danger'); + modal.closeModal(); + return; + }); + + }).catch(e => { + console.log(e); + _alert({ message: 'Please allow Gitcoin to connect to AlgoSigner extension' }, 'danger'); + modal.closeModal(); + return; + }); + } catch (e) { + modal.closeModal(); + _alert({ message: 'Unable to make contribution to grant. Please try again later.' }, 'error'); + console.log(error); + return; + } + + + function callback(error, from_address, txn) { + if (error) { + vm.updatePaymentStatus(grant.grant_id, 'failed'); + _alert({ message: gettext('Unable to contribute to grant due to ' + error) }, 'danger'); + console.log(error); + } else { + + const payload = { + 'contributions': [{ + 'grant_id': grant.grant_id, + 'contributor_address': from_address, + 'tx_id': txn, + 'token_symbol': grant.grant_donation_currency, + 'tenant': 'ALGORAND', + 'comment': grant.grant_comments, + 'amount_per_period': grant.grant_donation_amount + }] + }; + + const apiUrlBounty = 'v1/api/contribute'; + + fetchData(apiUrlBounty, 'POST', JSON.stringify(payload)).then(response => { + if (200 <= response.status && response.status <= 204) { + MauticEvent.createEvent({ + 'alias': 'products', + 'data': [ + { + 'name': 'product', + 'attributes': { + 'product': 'grants', + 'persona': 'grants-contributor', + 'action': 'contribute' + } + } + ] + }); + vm.updatePaymentStatus(grant.grant_id, 'done', txn); + } else { + vm.updatePaymentStatus(grant.grant_id, 'failed'); + _alert('Unable to make contribute to grant. Please try again later', 'danger'); + console.error(`error: grant contribution failed with status: ${response.status} and message: ${response.message}`); + } + }).catch(function(error) { + vm.updatePaymentStatus(grant.grant_id, 'failed'); + _alert('Unable to make contribute to grant. Please try again later', 'danger'); + console.log(error); + }); + } + } +}; diff --git a/app/assets/v2/js/grants/funding.js b/app/assets/v2/js/grants/funding.js index 178fdfa34ad..938bab1b2dd 100644 --- a/app/assets/v2/js/grants/funding.js +++ b/app/assets/v2/js/grants/funding.js @@ -157,6 +157,9 @@ function tokenOptionsForGrant(grant) { } else if (grant.tenants && grant.tenants.includes('RSK')) { tokenDataList = tokenDataList.filter(token => token.chainId === 30); tokenDefault = 'RBTC'; + } else if (grant.tenants && grant.tenants.includes('ALGORAND')) { + tokenDataList = tokenDataList.filter(token => token.chainId === 1001); + tokenDefault = 'ALGO'; } else { tokenDataList = tokenDataList.filter((token) => token.chainId === 1); } diff --git a/app/assets/v2/js/pages/bounty_detail/algorand_extension.js b/app/assets/v2/js/pages/bounty_detail/algorand_extension.js index cec1b28beda..16ecd9edb1a 100644 --- a/app/assets/v2/js/pages/bounty_detail/algorand_extension.js +++ b/app/assets/v2/js/pages/bounty_detail/algorand_extension.js @@ -16,7 +16,6 @@ const payWithAlgorandExtension = async(fulfillment_id, to_address, vm, modal) => return; } - // const connect = await AlgoSigner.connect(); AlgoSigner.connect().then(async() => { // step2: get connected accounts const accounts = await AlgoSigner.accounts({ ledger: NETWORK }); diff --git a/app/assets/v2/js/pages/bounty_details2.js b/app/assets/v2/js/pages/bounty_details2.js index df33f6a7f3e..6ae407720f0 100644 --- a/app/assets/v2/js/pages/bounty_details2.js +++ b/app/assets/v2/js/pages/bounty_details2.js @@ -229,7 +229,7 @@ Vue.mixin({ case 'ALGO': case 'USDTa': case 'USDCa': - url = `https://algoexplorer.io/tx/${address}`; + url = `https://algoexplorer.io/address/${address}`; break; case 'SC': diff --git a/app/grants/admin.py b/app/grants/admin.py index bd161d82253..2a8aae97681 100644 --- a/app/grants/admin.py +++ b/app/grants/admin.py @@ -113,7 +113,7 @@ class GrantAdmin(GeneralAdmin): 'metadata', 'twitter_handle_1', 'twitter_handle_2', 'view_count', 'in_active_clrs', 'last_update', 'funding_info', 'twitter_verified', 'twitter_verified_by', 'twitter_verified_at', 'stats_history', 'zcash_payout_address', 'celo_payout_address','zil_payout_address', 'harmony_payout_address', 'binance_payout_address', - 'polkadot_payout_address', 'kusama_payout_address', 'rsk_payout_address', 'emails', 'admin_message', 'has_external_funding' + 'polkadot_payout_address', 'kusama_payout_address', 'rsk_payout_address', 'algorand_payout_address', 'emails', 'admin_message', 'has_external_funding' ] readonly_fields = [ 'logo_svg_asset', 'logo_asset', diff --git a/app/grants/management/commands/sync_pending_contributions.py b/app/grants/management/commands/sync_pending_contributions.py index e9c3ee5ac8d..2c453abed57 100644 --- a/app/grants/management/commands/sync_pending_contributions.py +++ b/app/grants/management/commands/sync_pending_contributions.py @@ -42,7 +42,7 @@ def handle(self, *args, **options): # Auto expire pending transactions timeout_period = timezone.now() - timedelta(minutes=60) - tenants = ['ZCASH', 'ZIL', 'CELO', 'POLKADOT', 'HARMONY', 'BINANCE', 'KUSAMA', 'RSK'] + tenants = ['ZCASH', 'ZIL', 'CELO', 'POLKADOT', 'HARMONY', 'BINANCE', 'KUSAMA', 'RSK', 'ALGORAND'] for tenant in tenants: tenant_pending_contributions = pending_contribution.filter(subscription__tenant=tenant) diff --git a/app/grants/models.py b/app/grants/models.py index 6a0abce3ad6..7f3eb5920da 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -414,6 +414,13 @@ class Meta: blank=True, help_text=_('The rsk wallet address where subscription funds will be sent.'), ) + algorand_payout_address = models.CharField( + max_length=255, + default='0x0', + null=True, + blank=True, + help_text=_('The algorand wallet address where subscription funds will be sent.'), + ) # TODO-GRANTS: remove contract_owner_address = models.CharField( max_length=255, @@ -650,6 +657,8 @@ def tenants(self): tenants.append('BINANCE') if self.rsk_payout_address and self.rsk_payout_address != '0x0': tenants.append('RSK') + if self.algorand_payout_address and self.algorand_payout_address != '0x0': + tenants.append('ALGORAND') return tenants @@ -892,6 +901,7 @@ def cart_payload(self, build_absolute_uri, user=None): 'kusama_payout_address': self.kusama_payout_address, 'harmony_payout_address': self.harmony_payout_address, 'rsk_payout_address': self.rsk_payout_address, + 'algorand_payout_address': self.algorand_payout_address, 'is_on_team': is_grant_team_member(self, user.profile) if user and user.is_authenticated else False, } @@ -950,6 +960,7 @@ def repr(self, user, build_absolute_uri): 'harmony_payout_address': self.harmony_payout_address, 'binance_payout_address': self.binance_payout_address, 'rsk_payout_address': self.rsk_payout_address, + 'algorand_payout_address': self.algorand_payout_address, 'token_address': self.token_address, 'image_css': self.image_css, 'verified': self.twitter_verified, @@ -1007,7 +1018,8 @@ class Subscription(SuperModel): ('KUSAMA', 'KUSAMA'), ('HARMONY', 'HARMONY'), ('BINANCE', 'BINANCE'), - ('RSK', 'RSK') + ('RSK', 'RSK'), + ('ALGORAND', 'ALGORAND') ] active = models.BooleanField(default=True, db_index=True, help_text=_('Whether or not the Subscription is active.')) @@ -1679,7 +1691,8 @@ class Contribution(SuperModel): ('polkadot_std', 'polkadot_std'), ('harmony_std', 'harmony_std'), ('binance_std', 'binance_std'), - ('rsk_std', 'rsk_std') + ('rsk_std', 'rsk_std'), + ('algorand_std', 'algorand_std') ] success = models.BooleanField(default=True, help_text=_('Whether or not success.')) diff --git a/app/grants/sync/algorand.py b/app/grants/sync/algorand.py new file mode 100644 index 00000000000..d5621ec62e3 --- /dev/null +++ b/app/grants/sync/algorand.py @@ -0,0 +1,106 @@ +from django.conf import settings + +import requests +from grants.sync.helpers import record_contribution_activity, txn_already_used + +API_KEY = settings.ALGORAND_API_KEY + + +def get_algorand_txn_status_paid_explorer(contribution): + txnid = contribution.tx_id + + if not txnid or txnid == "0x0": + return None + + subscription = contribution.subscription + grant = subscription.grant + token_symbol = subscription.token_symbol + amount = subscription.amount_per_period + + to_address = grant.algorand_payout_address + from_address = subscription.contributor_address + + headers = { + 'accept': 'application/json', + 'x-api-key': API_KEY + } + + url = f'https://mainnet-algorand.api.purestake.io/idx2/v2/transactions/{txnid}' + response = requests.get(url=url, headers=headers).json() + + if response: + if response.get("current-round") and response.get("transaction"): + txn = response["transaction"] + + # asset / algo token + payment_txn = txn['payment-transaction'] if txn.get('payment-transaction') else txn['asset-transfer-transaction'] + + if ( + txn['confirmed-round'] > 0 and + txn['sender'].lower() == from_address.lower() and + payment_txn['receiver'].lower() == to_address.lower() and + float(float(payment_txn['amount'])/ 10**6) == float(amount) and + not txn_already_used(txnid, token_symbol) + ): + return 'success' + + return None + + +def get_algorand_txn_status(contribution): + txnid = contribution.tx_id + + if not txnid or txnid == "0x0": + return None + + subscription = contribution.subscription + grant = subscription.grant + token_symbol = subscription.token_symbol + amount = subscription.amount_per_period + + to_address = grant.algorand_payout_address + from_address = subscription.contributor_address + + url = f'https://api.algoexplorer.io/v2/transactions/pending/{txnid}?format=json' + + response = requests.get(url).json() + + if response.get('confirmed-round') and response.get('txn') and response.get('txn').get('txn'): + txn = response["txn"]['txn'] + if not response["pool-error"] == "": + return None + + # asset / algo token + rcv = txn['rcv'] if txn.get('rcv') else txn['arcv'] + amt = txn['amt'] if txn.get('amt') else txn['aamt'] + + if ( + txn['snd'].lower() == from_address.lower() and + rcv.lower() == to_address.lower() and + float(float(amt)/ 10**6) == float(amount) and + not txn_already_used(txnid, token_symbol) + ): + return 'success' + + elif response.get('message') and API_KEY != '': + # txn is too old and cannot be found on algoexplorer + return get_algorand_txn_status_paid_explorer(contribution) + + return None + + +def sync_algorand_payout(contribution): + + if contribution.tx_id and contribution.tx_id != '0x0': + txn_status = get_algorand_txn_status(contribution) + + if txn_status == 'success': + contribution.success = True + contribution.tx_cleared = True + contribution.checkout_type = 'alogrand_std' + record_contribution_activity(contribution) + contribution.save() + else: + contribution.success = True + contribution.tx_cleared = False + contribution.save() diff --git a/app/grants/templates/grants/_new.html b/app/grants/templates/grants/_new.html index 74baac7a6d5..436e8494de0 100644 --- a/app/grants/templates/grants/_new.html +++ b/app/grants/templates/grants/_new.html @@ -236,6 +236,10 @@
Funding Information
+ + {% endif %} @@ -399,6 +403,24 @@
Funding Information
[[errors.rsk_payout_address]] + + + diff --git a/app/grants/templates/grants/bulk_add_to_cart.html b/app/grants/templates/grants/bulk_add_to_cart.html index fae912a17a0..377b1418f36 100644 --- a/app/grants/templates/grants/bulk_add_to_cart.html +++ b/app/grants/templates/grants/bulk_add_to_cart.html @@ -30,6 +30,7 @@ kusama_payout_address: "{{ grant.obj.kusama_payout_address }}", binance_payout_address: "{{ grant.obj.binance_payout_address }}", rsk_payout_address: "{{ grant.obj.rsk_payout_address }}", + algorand_payout_address: "{{ grant.obj.algorand_payout_address }}", grant_token_address: "{{ grant.obj.token_address }}", grant_logo: {% if grant.logo and grant.logo.url %}"{{ grant.obj.logo.url }}"{% else %}{% with grant_logo='v2/images/grants/logos/' id=grant.id|modulo:3 %} "{% static grant_logo|addstr:id|add:'.png' %}" {% endwith %} {% endif %}, grant_clr_prediction_curve: "{{ grant.obj.clr_prediction_curve }}", diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index 98a648b89a8..922fff3bfe6 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -108,6 +108,9 @@

Grants Cart

{% include 'grants/cart/extension/rsk.html' %} + + {% include 'grants/cart/extension/algorand.html' %} + + + + diff --git a/app/grants/utils.py b/app/grants/utils.py index 25f02c82118..25c78ed34b4 100644 --- a/app/grants/utils.py +++ b/app/grants/utils.py @@ -30,10 +30,12 @@ from django.utils import timezone from app import settings +from app.dashboard.sync.algorand import sync_algorand_payout from app.settings import BASE_DIR, BASE_URL, MEDIA_URL, STATIC_HOST, STATIC_URL from avatar.utils import convert_img from economy.utils import ConversionRateNotFoundError, convert_amount from gas.utils import eth_usd_conv_rate +from grants.sync.algorand import sync_algorand_payout from grants.sync.binance import sync_binance_payout from grants.sync.celo import sync_celo_payout from grants.sync.harmony import sync_harmony_payout @@ -58,7 +60,8 @@ 'POLKADOT': sync_polkadot_payout, 'BINANCE': sync_binance_payout, 'KUSAMA': sync_polkadot_payout, - 'RSK': sync_rsk_payout + 'RSK': sync_rsk_payout, + 'ALGORAND': sync_algorand_payout } def get_clr_rounds_metadata(): diff --git a/app/grants/views.py b/app/grants/views.py index a8d1b6d43c9..d987b32c7a0 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -1594,6 +1594,7 @@ def grant_edit(request, grant_id): kusama_payout_address = request.POST.get('kusama_payout_address', '0x0') binance_payout_address = request.POST.get('binance_payout_address', '0x0') rsk_payout_address = request.POST.get('rsk_payout_address', '0x0') + algorand_payout_address = request.POST.get('algorand_payout_address', '0x0') if ( eth_payout_address == '0x0' and @@ -1604,7 +1605,8 @@ def grant_edit(request, grant_id): kusama_payout_address == '0x0' and harmony_payout_address == '0x0' and binance_payout_address == '0x0' and - rsk_payout_address == '0x0' + rsk_payout_address == '0x0' and + algorand_payout_address == '0x0' ): response['message'] = 'error: payout_address is a mandatory parameter' return JsonResponse(response) @@ -1643,6 +1645,9 @@ def grant_edit(request, grant_id): if rsk_payout_address != '0x0': grant.rsk_payout_address = rsk_payout_address + if algorand_payout_address != '0x0': + grant.algorand_payout_address = algorand_payout_address + github_project_url = request.POST.get('github_project_url', None) if github_project_url: grant.github_project_url = github_project_url @@ -1788,13 +1793,14 @@ def grant_new(request): harmony_payout_address = request.POST.get('harmony_payout_address', None) binance_payout_address = request.POST.get('binance_payout_address', None) rsk_payout_address = request.POST.get('rsk_payout_address', None) + algorand_payout_address = request.POST.get('algorand_payout_address', None) if ( not eth_payout_address and not zcash_payout_address and not celo_payout_address and not zil_payout_address and not polkadot_payout_address and not kusama_payout_address and not harmony_payout_address and not binance_payout_address and - not rsk_payout_address + not rsk_payout_address and not algorand_payout_address ): response['message'] = 'error: payout_address is a mandatory parameter' return JsonResponse(response) @@ -1843,6 +1849,7 @@ def grant_new(request): 'harmony_payout_address': harmony_payout_address if harmony_payout_address else '0x0', 'binance_payout_address': binance_payout_address if binance_payout_address else '0x0', 'rsk_payout_address': rsk_payout_address if rsk_payout_address else '0x0', + 'algorand_payout_address': algorand_payout_address if algorand_payout_address else '0x0', 'token_symbol': token_symbol, 'contract_version': contract_version, 'deploy_tx_id': request.POST.get('transaction_hash', '0x0'), @@ -3145,7 +3152,7 @@ def contribute_to_grants_v1(request): }) continue - if not tenant in ['ETH', 'ZCASH', 'ZIL', 'CELO', 'POLKADOT', 'HARMONY', 'KUSAMA', 'BINANCE', 'RSK']: + if not tenant in ['ETH', 'ZCASH', 'ZIL', 'CELO', 'POLKADOT', 'HARMONY', 'KUSAMA', 'BINANCE', 'RSK', 'ALGORAND']: invalid_contributions.append({ 'grant_id': grant_id, 'message': 'error: tenant chain is not supported for grant' From 4515c31c3e47606aee9fe856b609597faa5af6b5 Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Tue, 22 Jun 2021 17:31:12 +0530 Subject: [PATCH 2/5] chore: tweak frontend to allow users to payout grants --- app/assets/v2/js/cart.js | 10 +- .../v2/js/grants/cart/algorand_extension.js | 112 +++++++++--------- .../pages/bounty_detail/algorand_extension.js | 6 +- .../grants/cart/extension/algorand.html | 27 ++++- .../templates/grants/components/card.html | 1 + app/grants/utils.py | 2 - 6 files changed, 91 insertions(+), 67 deletions(-) diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index 85e2cea4495..aed40e2fd3c 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -92,6 +92,7 @@ Vue.component('grants-cart', { `${static_url}v2/js/grants/cart/rsk_extension.js` ], 'ALGORAND': [ + `${static_url}v2/js/tokens.js`, `${static_url}v2/js/grants/cart/algorand_extension.js` ] } @@ -555,7 +556,11 @@ Vue.component('grants-cart', { switch (tenant) { case 'ALGORAND': - contributeWithAlgorandExtension(grant, vm); + if (data) { + contributeWithAlgorandExtension(grant, vm, data); + } else { + initAlgorandConnection(grant, vm); + } break; case 'RSK': contributeWithRskExtension(grant, vm); @@ -623,9 +628,12 @@ Vue.component('grants-cart', { } if (additionalAttributes) { vm.grantData[index].additionalAttributes = additionalAttributes; + console.log(vm.grantData[index]); } } }); + + vm.$forceUpdate(); }, /** diff --git a/app/assets/v2/js/grants/cart/algorand_extension.js b/app/assets/v2/js/grants/cart/algorand_extension.js index e64a952a1ec..9d1eeb084c2 100644 --- a/app/assets/v2/js/grants/cart/algorand_extension.js +++ b/app/assets/v2/js/grants/cart/algorand_extension.js @@ -1,38 +1,39 @@ -const contributeWithAlgorandExtension = async(grant, vm, modal) => { - const token_name = grant.grant_donation_currency; - const amount = grant.grant_donation_amount; - const to_address = grant.rsk_payout_address; - const token = vm.getTokenByName(token_name); - // TODO: FIGURE OUT from_address - - // const NETWORK = 'TestNet'; - const NETWORK = 'MainNet'; +const initAlgorandConnection = async(grant, vm) => { // 1. check if AlgoSigner is available if (!AlgoSigner) { _alert({ message: 'Please download or enable AlgoSigner extension' }, 'danger'); - modal.closeModal(); return; } - try { - AlgoSigner.connect().then(async() => { - // step2: get connected accounts - const accounts = await AlgoSigner.accounts({ ledger: NETWORK }); + // const NETWORK = 'TestNet'; + const NETWORK = 'MainNet'; - let is_account_present = false; + // step2: get connected accounts + AlgoSigner.connect().then(async() => { + const addresses = await AlgoSigner.accounts({ ledger: NETWORK }); - accounts.map(account=> { - if (account.address == from_address) - is_account_present = true; - }); - - if (!is_account_present) { - _alert({ message: `Unable to access address ${from_address} in wallet` }, 'danger'); - modal.closeModal(); - return; - } + if (addresses.length == 0) { + _alert({ message: 'No wallet addresses detected on AlgoSigner extension' }, 'danger'); + return; + } + vm.updatePaymentStatus(grant.grant_id, 'waiting-on-user-input', null, {addresses: addresses}); + }); + +}; + +const contributeWithAlgorandExtension = async(grant, vm, from_address) => { + const token_name = grant.grant_donation_currency; + const amount = grant.grant_donation_amount; + const to_address = grant.algorand_payout_address; + const token = vm.getTokenByName(token_name); + + // const NETWORK = 'TestNet'; + const NETWORK = 'MainNet'; + + try { + AlgoSigner.connect().then(async() => { // step3: check if enough balance is present const balance = await AlgoSigner.algod({ @@ -46,41 +47,39 @@ const contributeWithAlgorandExtension = async(grant, vm, modal) => { ) { // ALGO token _alert({ message: `Insufficent balance in address ${from_address}` }, 'danger'); - modal.closeModal(); return; - } // ALGO assets let is_asset_present = false; if (balance.assets && balance.assets.length > 0) { balance.assets.map(asset => { - if (asset['asset-id'] == asset_index) + if (asset['asset-id'] == token.addr) is_asset_present = true; }); } - if (is_asset_present) { + if (!is_asset_present) { _alert({ message: `Asset ${token_name} is not present in ${from_address}` }, 'danger'); - modal.closeModal(); return; } let has_enough_asset_balance = false; balance.assets.map(asset => { - if (asset['asset-id'] == asset_index && asset['amount'] <= amount * 10 ** token.decimals) + if (asset['asset-id'] == token.addr && asset['amount'] <= amount * 10 ** token.decimals) has_enough_asset_balance = true; }); if (has_enough_asset_balance) { _alert({ message: `Insufficent balance in address ${from_address}` }, 'danger'); - modal.closeModal(); return; } + // step4: set modal to waiting state + vm.updatePaymentStatus(grant.grant_id, 'waiting'); - // step4: get txnParams + // step5: get txnParams const txParams = await AlgoSigner.algod({ ledger: NETWORK, path: '/v2/transactions/params' @@ -92,8 +91,8 @@ const contributeWithAlgorandExtension = async(grant, vm, modal) => { if (token_name == 'ALGO') { // ALGO token txn = { - from: from_address, - to: to_address, + from: from_address.toUpperCase(), + to: to_address.toUpperCase(), fee: txParams['fee'], type: 'pay', amount: amount * 10 ** token.decimals, @@ -106,9 +105,9 @@ const contributeWithAlgorandExtension = async(grant, vm, modal) => { } else { // ALGO assets txn = { - from: from_address, - to: to_address, - assetIndex: Number(asset_index.addr), + from: from_address.toUpperCase(), + to: to_address.toUpperCase(), + assetIndex: Number(token.addr), note: 'contributing to gitcoin grant', amount: amount * 10 ** token.decimals, type: 'axfer', @@ -121,39 +120,34 @@ const contributeWithAlgorandExtension = async(grant, vm, modal) => { } AlgoSigner.sign(txn).then(signedTx => { - - // step6: broadcast txn + // step7: broadcast txn AlgoSigner.send({ ledger: NETWORK, tx: signedTx.blob - }) - .then(tx => { - callback(null, from_address, tx.txId); - }) - .catch((e) => { - console.log(e); - _alert({ message: 'Unable to broadcast txn. Please try again' }, 'danger'); - modal.closeModal(); - return; - }); + }).then(tx => { + callback(null, from_address, tx.txId); + }).catch((e) => { + console.log(e); + _alert({ message: 'Unable to broadcast txn. Please try again' }, 'danger'); + vm.updatePaymentStatus(grant.grant_id, 'failed'); + return; + }); }).catch(e => { console.log(e); _alert({ message: 'Unable to sign txn. Please try again' }, 'danger'); - modal.closeModal(); + vm.updatePaymentStatus(grant.grant_id, 'failed'); return; }); }).catch(e => { console.log(e); _alert({ message: 'Please allow Gitcoin to connect to AlgoSigner extension' }, 'danger'); - modal.closeModal(); + vm.updatePaymentStatus(grant.grant_id, 'failed'); return; }); } catch (e) { - modal.closeModal(); - _alert({ message: 'Unable to make contribution to grant. Please try again later.' }, 'error'); - console.log(error); + callback(err); return; } @@ -180,7 +174,9 @@ const contributeWithAlgorandExtension = async(grant, vm, modal) => { const apiUrlBounty = 'v1/api/contribute'; fetchData(apiUrlBounty, 'POST', JSON.stringify(payload)).then(response => { + if (200 <= response.status && response.status <= 204) { + console.log('success', response); MauticEvent.createEvent({ 'alias': 'products', 'data': [ @@ -194,15 +190,17 @@ const contributeWithAlgorandExtension = async(grant, vm, modal) => { } ] }); + vm.updatePaymentStatus(grant.grant_id, 'done', txn); + } else { vm.updatePaymentStatus(grant.grant_id, 'failed'); - _alert('Unable to make contribute to grant. Please try again later', 'danger'); + _alert('Unable to contribute to grant. Please try again later', 'danger'); console.error(`error: grant contribution failed with status: ${response.status} and message: ${response.message}`); } }).catch(function(error) { vm.updatePaymentStatus(grant.grant_id, 'failed'); - _alert('Unable to make contribute to grant. Please try again later', 'danger'); + _alert('Unable to contribute to grant. Please try again later', 'danger'); console.log(error); }); } diff --git a/app/assets/v2/js/pages/bounty_detail/algorand_extension.js b/app/assets/v2/js/pages/bounty_detail/algorand_extension.js index 16ecd9edb1a..daa025d2df0 100644 --- a/app/assets/v2/js/pages/bounty_detail/algorand_extension.js +++ b/app/assets/v2/js/pages/bounty_detail/algorand_extension.js @@ -53,12 +53,12 @@ const payWithAlgorandExtension = async(fulfillment_id, to_address, vm, modal) => if (balance.assets && balance.assets.length > 0) { balance.assets.map(asset => { - if (asset['asset-id'] == asset_index) + if (asset['asset-id'] == asset_index.addr) is_asset_present = true; }); } - if (is_asset_present) { + if (!is_asset_present) { _alert({ message: `Asset ${token_name} is not present in ${from_address}` }, 'danger'); modal.closeModal(); return; @@ -67,7 +67,7 @@ const payWithAlgorandExtension = async(fulfillment_id, to_address, vm, modal) => let has_enough_asset_balance = false; balance.assets.map(asset => { - if (asset['asset-id'] == asset_index && asset['amount'] <= amount * 10 ** vm.decimals) + if (asset['asset-id'] == asset_index.addr && asset['amount'] <= amount * 10 ** vm.decimals) has_enough_asset_balance = true; }); diff --git a/app/grants/templates/grants/cart/extension/algorand.html b/app/grants/templates/grants/cart/extension/algorand.html index ae37f971b0f..d6e0fcdf03f 100644 --- a/app/grants/templates/grants/cart/extension/algorand.html +++ b/app/grants/templates/grants/cart/extension/algorand.html @@ -60,8 +60,8 @@ Not Eligible for matching -
- +
+ Payout
@@ -76,7 +76,7 @@ {% include '../comment.html' %} -
+

@@ -89,9 +89,28 @@

Signing Transaction Failed.

+

- Try again in some time + Try again in some time +

+
+
+

+ Select the account you want to pay from

+ +
    +
  • + +
  • +
+

diff --git a/app/grants/templates/grants/components/card.html b/app/grants/templates/grants/components/card.html index 76830844bc7..6882bd79c12 100644 --- a/app/grants/templates/grants/components/card.html +++ b/app/grants/templates/grants/components/card.html @@ -218,6 +218,7 @@

+

diff --git a/app/grants/utils.py b/app/grants/utils.py index 25c78ed34b4..85528e8476f 100644 --- a/app/grants/utils.py +++ b/app/grants/utils.py @@ -29,8 +29,6 @@ from django.templatetags.static import static from django.utils import timezone -from app import settings -from app.dashboard.sync.algorand import sync_algorand_payout from app.settings import BASE_DIR, BASE_URL, MEDIA_URL, STATIC_HOST, STATIC_URL from avatar.utils import convert_img from economy.utils import ConversionRateNotFoundError, convert_amount From 2c427be845a5bd911958ed3474e723fc86fe32cd Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Wed, 23 Jun 2021 17:18:10 +0530 Subject: [PATCH 3/5] add migration --- .../migrations/0120_auto_20210622_1021.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/grants/migrations/0120_auto_20210622_1021.py diff --git a/app/grants/migrations/0120_auto_20210622_1021.py b/app/grants/migrations/0120_auto_20210622_1021.py new file mode 100644 index 00000000000..63f4954844e --- /dev/null +++ b/app/grants/migrations/0120_auto_20210622_1021.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.20 on 2021-06-22 10:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('grants', '0119_auto_20210611_0843'), + ] + + operations = [ + migrations.AddField( + model_name='grant', + name='algorand_payout_address', + field=models.CharField(blank=True, default='0x0', help_text='The algorand wallet address where subscription funds will be sent.', max_length=255, null=True), + ), + migrations.AlterField( + model_name='contribution', + name='checkout_type', + field=models.CharField(blank=True, choices=[('eth_std', 'eth_std'), ('eth_zksync', 'eth_zksync'), ('zcash_std', 'zcash_std'), ('celo_std', 'celo_std'), ('zil_std', 'zil_std'), ('polkadot_std', 'polkadot_std'), ('harmony_std', 'harmony_std'), ('binance_std', 'binance_std'), ('rsk_std', 'rsk_std'), ('algorand_std', 'algorand_std')], help_text='The checkout method used while making the contribution', max_length=30, null=True), + ), + migrations.AlterField( + model_name='subscription', + name='tenant', + field=models.CharField(blank=True, choices=[('ETH', 'ETH'), ('ZCASH', 'ZCASH'), ('CELO', 'CELO'), ('ZIL', 'ZIL'), ('POLKADOT', 'POLKADOT'), ('KUSAMA', 'KUSAMA'), ('HARMONY', 'HARMONY'), ('BINANCE', 'BINANCE'), ('RSK', 'RSK'), ('ALGORAND', 'ALGORAND')], default='ETH', help_text='specific tenant in which contribution is made', max_length=10, null=True), + ), + ] From 88fcbadf1ddcb017f05484c686fd5928070df67b Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Mon, 28 Jun 2021 18:27:58 +0530 Subject: [PATCH 4/5] address comment --- app/assets/v2/js/cart.js | 1 - app/assets/v2/js/grants/cart/algorand_extension.js | 2 +- app/assets/v2/js/pages/bounty_detail/algorand_extension.js | 2 +- app/grants/templates/grants/cart/extension/algorand.html | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index aed40e2fd3c..6721835f713 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -628,7 +628,6 @@ Vue.component('grants-cart', { } if (additionalAttributes) { vm.grantData[index].additionalAttributes = additionalAttributes; - console.log(vm.grantData[index]); } } }); diff --git a/app/assets/v2/js/grants/cart/algorand_extension.js b/app/assets/v2/js/grants/cart/algorand_extension.js index 9d1eeb084c2..dac8a1e9cea 100644 --- a/app/assets/v2/js/grants/cart/algorand_extension.js +++ b/app/assets/v2/js/grants/cart/algorand_extension.js @@ -135,7 +135,7 @@ const contributeWithAlgorandExtension = async(grant, vm, from_address) => { }).catch(e => { console.log(e); - _alert({ message: 'Unable to sign txn. Please try again' }, 'danger'); + _alert({ message: 'Unable to sign transaction. Please try again' }, 'danger'); vm.updatePaymentStatus(grant.grant_id, 'failed'); return; }); diff --git a/app/assets/v2/js/pages/bounty_detail/algorand_extension.js b/app/assets/v2/js/pages/bounty_detail/algorand_extension.js index daa025d2df0..b79c1a2ac92 100644 --- a/app/assets/v2/js/pages/bounty_detail/algorand_extension.js +++ b/app/assets/v2/js/pages/bounty_detail/algorand_extension.js @@ -137,7 +137,7 @@ const payWithAlgorandExtension = async(fulfillment_id, to_address, vm, modal) => }).catch(e => { console.log(e); - _alert({ message: 'Unable to sign txn. Please try again' }, 'danger'); + _alert({ message: 'Unable to sign transaction. Please try again' }, 'danger'); modal.closeModal(); return; }); diff --git a/app/grants/templates/grants/cart/extension/algorand.html b/app/grants/templates/grants/cart/extension/algorand.html index d6e0fcdf03f..db8398bb933 100644 --- a/app/grants/templates/grants/cart/extension/algorand.html +++ b/app/grants/templates/grants/cart/extension/algorand.html @@ -3,7 +3,7 @@

- You’ll need a Algorand extension to contribute to Algorand grants. We recommend + You’ll need an Algorand extension to contribute to Algorand grants. We recommend AlgoSigner Wallet.

From cbd6ff283515840898cc15aa7ad30da5d509b209 Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Mon, 28 Jun 2021 18:38:37 +0530 Subject: [PATCH 5/5] Update algorand_extension.js --- app/assets/v2/js/grants/cart/algorand_extension.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/v2/js/grants/cart/algorand_extension.js b/app/assets/v2/js/grants/cart/algorand_extension.js index dac8a1e9cea..0e58c694e67 100644 --- a/app/assets/v2/js/grants/cart/algorand_extension.js +++ b/app/assets/v2/js/grants/cart/algorand_extension.js @@ -128,7 +128,7 @@ const contributeWithAlgorandExtension = async(grant, vm, from_address) => { callback(null, from_address, tx.txId); }).catch((e) => { console.log(e); - _alert({ message: 'Unable to broadcast txn. Please try again' }, 'danger'); + _alert({ message: 'Unable to broadcast transaction. Please try again' }, 'danger'); vm.updatePaymentStatus(grant.grant_id, 'failed'); return; });