UNPKG

tronair-gui

Version:

Web application/GUI for performing airdrops on the TRON blockchain

855 lines (775 loc) 26.2 kB
import React, { Component } from 'react'; import { Button, Icon, Modal, Input, Row, Col, Progress, notification, message } from 'antd'; import {CopyToClipboard} from 'react-copy-to-clipboard'; import TronWeb from 'tronweb'; import Swal from 'sweetalert2'; import "antd/dist/antd.css"; import './App.css'; export default class Submitoptions extends Component { // This component doesn't use any properties constructor(props) { super(props); this.inputOpenFileRef = React.createRef() this.state = { save_check: 'false', save_dir_txt: (this.props.appActions.dataSlots ? this.props.appActions.dataSlots['ds_save_dir'] : '') || '', open_file_browser: false, airdrop_list: (this.props.appActions.dataSlots ? this.props.appActions.dataSlots['ds_airdrop_array'] : []) || [], poollog: '', file_name: 'airdrop_data', isOpen: false, adrop_open: false, adrop_btn_dis: true, pct_complete: 100, fn_valid: true, drop_start: false, is_complete: false, num_success: 1, num_fail: 0, key: false, is_disabled: false, new_acc_updated: false, new_tl_acc: null, }; // console.log(this.state.airdrop_list); this.onDrop = this.onDrop.bind(this); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async switch_acc(addr,pkey,token_id) { await new Promise(resolve => { // window.tronWeb = new TronWeb( // process.env.REACT_APP_FULL_NODE, // process.env.REACT_APP_SOLIDITY_NODE, // process.env.REACT_APP_EVENT_SERVER, // pkey // ); window.tronWeb = new TronWeb( 'https://api.trongrid.io', 'https://api.trongrid.io', 'https://api.trongrid.io', pkey, ); return resolve(); }) var acc_trx = 0; var acc_token = 0; if (window.tronWeb.defaultAddress.base58 === addr) { // console.log('Address successfully switched! Getting balances..'); let tries = 0; var is_valid = false; while (tries < 10) { await window.tronWeb.trx.getUnconfirmedAccount(addr) // eslint-disable-next-line .then(acc_balance => { // console.log('Account balances:') // console.log(acc_balance); acc_trx = acc_balance.balance; acc_token = acc_balance.assetV2[0].value; if (acc_trx > 0 && acc_token > 0) { // console.log('Balances match! Returning true.'); tries = 10; is_valid = true; } else { // console.log('Balances do not match. Trying again...'); tries++; } }) // eslint-disable-next-line .catch(err => { console.log('Error getting balances: ' + err); tries++; }) // console.log('Sleeping 500ms...'); if (!is_valid) { await this.sleep(500); } } return is_valid; } else { // console.log('Addresses do not match. Returning false'); return false; } } numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } checkboxChanged_save_check = (event) => { this.setState({save_check: (event.target.checked ? 'true' : 'false')}); this.props.appActions.updateDataSlot("ds_save_data", (event.target.checked ? 'true' : 'false')); } textInputChanged_save_dir_txt = (event) => { this.setState({save_dir_txt: event.target.value}); this.props.appActions.updateDataSlot("ds_save_dir", event.target.value); } onClick_elSave_dir_browse = (ev) => { this.setState({open_file_browser:true}); } exportToJson(json_data, filename) { try { filename = filename + '.json'; let json_str = JSON.stringify(json_data,0,2); let d_str = URL.createObjectURL(new Blob([json_str],{type:'text/json'})); let a = document.createElement('a'); a.setAttribute('href', d_str); a.setAttribute('download', filename); let clk = new MouseEvent('click'); a.dispatchEvent(clk); } catch(err) { console.log(err); } } onClick_downloadBtn = (ev) => { this.exportToJson(this.props.appActions.dataSlots.ds_airdrop_array, this.state.file_name); this.closeModal(); } onClick_saveAll = (ev) => { const ds = this.props.appActions.dataSlots; // this.exportToJson(ds.ds_airdrop_array, this.state.file_name); const s_name = this.state.file_name + '_successes'; if (ds.ds_success_list.length > 0) { this.exportToJson(ds.ds_success_list, s_name); } const f_name = this.state.file_name + '_failures'; if (ds.ds_failure_list.length > 0) { this.exportToJson(ds.ds_failure_list, f_name); } if (ds.ds_login_method === 'tronlink') { const f_acc = this.state.file_name + '_new_account'; this.exportToJson(this.state.new_tl_acc, f_acc); } this.closeModal(); } run_airdrop = (ev) => { this.double_check(); } async doAirdropTRX(airdrop) { var success = []; var failures = []; let all = Promise.all(airdrop.map(async wallet => { try { let result = await window.tronWeb.trx.sendTrx(wallet.address, wallet.amount); wallet.msg = "SUCCESS"; wallet.tx = result.transaction.txID; // wallet.tx = signed_txn.txID; } catch(err) { wallet.msg = err; wallet.tx = ""; console.log('Broadcast failed. Trying again...'); try { let result = await window.tronWeb.trx.sendTrx(wallet.address, wallet.amount); wallet.msg = "SUCCESS"; wallet.tx = result.transaction.txID; } catch(err) { wallet.msg = err; wallet.tx = ""; console.log('Broadcast failed again. Error code: ' + err); } } return wallet; })).then(data => { data.forEach( tx => { if(tx.msg === "SUCCESS"){ success.push(tx); }else{ failures.push(tx); } }); return {success: success, failures: failures}; }); return all; } async doAirdropToken(airdrop, token_id) { var success = []; var failures = []; let all = Promise.all(airdrop.map(async wallet => { try { let result = await window.tronWeb.trx.sendToken(wallet.address, wallet.amount, token_id); wallet.msg = "SUCCESS"; wallet.tx = result.transaction.txID; } catch(err) { wallet.msg = err; wallet.tx = ""; console.log('Broadcast failed. Trying again...'); try { let result = await window.tronWeb.trx.sendToken(wallet.address, wallet.amount, token_id); wallet.msg = "SUCCESS"; wallet.tx = result.transaction.txID; } catch(err) { wallet.msg = err; wallet.tx = ""; console.log('Broadcast failed again. Error code: ' + err); } } return wallet; })).then(data => { data.forEach(tx => { if(tx.msg === "SUCCESS"){ success.push(tx); }else{ failures.push(tx); } }); return {success: success, failures: failures}; }); return all; } async send_leftovers(s_list,f_list) { const from_addr = window.tronWeb.defaultAddress.base58; const to_addr = this.props.appActions.dataSlots.ds_acc_addr; const acc_balance = await window.tronWeb.trx.getUnconfirmedAccount(from_addr); var acc_trx = acc_balance.balance - 500; var txn = {}; if (acc_trx > 0) { txn = {address: to_addr, amount: acc_trx}; try { let result = await window.tronWeb.trx.sendTrx(to_addr, acc_trx); txn.msg = "SUCCESS"; txn.tx = result.transaction.txID; s_list.push(txn); } catch(err) { txn.msg = err; txn.tx = ""; f_list.push(txn); } } const assets = acc_balance.assetV2; for (var i=0; i<assets.length; i++) { var token = assets[0]; if(token.value > 0) { txn = {address: to_addr, amount: token.value}; try { let result = await window.tronWeb.trx.sendToken(to_addr, token.value, token.key); txn.msg = "SUCCESS"; txn.tx = result.transaction.txID; s_list.push(txn); } catch(err) { txn.msg = err; txn.tx = ""; f_list.push(txn); } } } } async real_airdrop() { const ds = this.props.appActions.dataSlots; const adrop = ds.ds_airdrop_array; const is_trx = ds.ds_airdrop.trx; const tid = ds.ds_airdrop.token_id; const login_method = ds.ds_login_method; var chunk = Math.ceil(adrop.length/20); if (chunk > 20) {chunk=20;}; // var help_result = {result:true}; let help_result = await window.tronWeb.trx.sendToken(ds.ds_help_address,adrop.length,'1000562'); if (help_result.result) { var success_list = []; var fail_list = []; var i,j,item,chunk_arr; if (login_method === 'tronlink') { const new_acc = await window.tronWeb.createAccount(); this.setState({new_tl_acc: new_acc}); var trx_result = {result:true}; if (ds.ds_airdrop_trx > 0) { trx_result = {result:false}; await window.tronWeb.trx.sendTrx(new_acc.address.base58,ds.ds_airdrop_trx) .then(res => { trx_result = res; }) .catch(err => { console.log('Error sending TRX to new account: ' + err); console.log(window.tronWeb.defaultAddress.base58 + 'tried to send ' + new_acc.address.base58 + ' ' + ds.ds_airdrop_trx + ' TRX.'); }) } var token_result = {result:true}; if (ds.ds_airdrop_token > 0) { token_result = {result:false}; await window.tronWeb.trx.sendToken(new_acc.address.base58,ds.ds_airdrop_token,tid) .then(res => { token_result = res; }) .catch(err => { console.log('Error sending tokens to new account: ' + err); console.log(window.tronWeb.defaultAddress.base58 + 'tried to send ' + new_acc.address.base58 + ' ' + ds.ds_airdrop_token + ' tokens.'); }) } if (trx_result.result && token_result.result) { var acc_did_update = await this.switch_acc(new_acc.address.base58,new_acc.privateKey,tid); if (acc_did_update) { for (i=0,j=adrop.length; i<j; i+=chunk) { chunk_arr = adrop.slice(i,i+chunk); let result = {success:[],failures:[]}; if (is_trx) { result = await this.doAirdropTRX(chunk_arr); } else { result = await this.doAirdropToken(chunk_arr,tid); } for (item in result.success) { success_list.push(result.success[item]); } for (item in result.failures) { fail_list.push(result.failures[item]); } this.props.appActions.updateDataSlot('ds_success_list', success_list); this.props.appActions.updateDataSlot('ds_failure_list', fail_list); } await this.send_leftovers(success_list,fail_list) .catch(err => { console.log('Error sending leftovers: ' + err); }) } } this.forceUpdate(); this.setState({is_complete: true}); } else { for (i=0,j=adrop.length; i<j; i+=chunk) { chunk_arr = adrop.slice(i,i+chunk); let result = {success:[],failures:[]}; if (is_trx) { result = await this.doAirdropTRX(chunk_arr); } else { result = await this.doAirdropToken(chunk_arr,tid); } for (item in result.success) { success_list.push(result.success[item]); } for (item in result.failures) { fail_list.push(result.failures[item]); } this.props.appActions.updateDataSlot('ds_success_list', success_list); this.props.appActions.updateDataSlot('ds_failure_list', fail_list); } this.setState({is_complete: true}); this.forceUpdate(); } } else { console.log(help_result); this.notify_no_help(''); } } async notify_complete() { var temp_str = '<p><strong>Thank you for using the Community Node Airdrop Tool!</strong></p>'; temp_str += '<p>If you have any comments or suggestions about improvements, feel free to email us at '; temp_str += '<a href="https://mail.google.com/mail/u/0/?view=cm&fs=1&tf=1&to=communitynode@gmail.com'; temp_str += '&su=Airdrop%20GUI%20Completion%20Survey" target="_blank">communitynode@gmail.com</a>.</p>'; var {value: text} = await Swal.fire({ title: 'Airdrop Complete!', html: temp_str, confirmButtonText: 'Logout', }); if (text) { this.props.appActions.logoutCallback(); } } async notify_no_help(err_msg) { const ds = this.props.appActions.dataSlots; const help_total = ds.ds_help_total; const adrop = ds.ds_airdrop_array; var temp_str = '<p><strong>Unable to transfer HELP tokens...</strong></p>'; temp_str += '<p>Your current HELP token (ID: 1000562) balance is '; temp_str += this.numberWithCommas(help_total) + ' and this airdrop required ' + this.numberWithCommas(adrop.length) + ' tokens. '; temp_str += '<p>Please make sure you meet all of the airdrop requirements and try again.</p>'; await Swal.fire({ type: 'warning', html: temp_str, confirmButtonText: 'OK', }); } async double_check() { const ds = this.props.appActions.dataSlots; const help_cost = ds.ds_airdrop_array.length; const tname = ds.ds_airdrop.token_name; var damt = 0; if (ds.ds_drop_type.type !== 'total') { damt = ds.ds_drop_amount*help_cost; } else { ds.ds_airdrop_array.forEach(wallet => { damt += wallet.amount; }); }; const tpr = ds.ds_airdrop.token_precision; if (tpr > 0) { damt = Math.round((damt/Math.pow(10,tpr))*100)/100; } var temp_str = '<p><strong>ARE YOU SURE?</strong></p>'; temp_str += '<p>This airdrop will cost you the following amounts:</p>'; temp_str += '<p><strong>HELP: ' + this.numberWithCommas(help_cost) + '</strong></p>'; temp_str += '<p><strong>' + tname + ': ' + this.numberWithCommas(damt) + '</strong></p>'; temp_str += "<p>You can not un-do an airdrop, so make sure you have double-checked everything before pressing 'Yes'</p>"; if (ds.ds_login_method === 'tronlink') { // temp_str += "<p><strong>Because you are using TronLink, you will have to approve all " // temp_str += this.numberWithCommas(help_cost) + " transactions by hand.</strong></p>"; temp_str += "<p><strong>Because you are using TronLink, you will have to approve the first few transactions by hand.<br />" temp_str += "Also, be sure you save any private keys if an account is created for you. They will be deleted once you log out.</strong></p>"; } Swal.fire({ type: 'info', html: temp_str, confirmButtonText: 'Yes', cancelButtonText: 'No', showCancelButton: true, reverseButtons:true, }).then((result) => { if (result.value) { this.props.appActions.updateDataSlot('ds_drop_start',true); this.setState({drop_start: true}) this.real_airdrop(); } else if (result.dismiss === Swal.DismissReason.cancel) { this.props.appActions.updateDataSlot('ds_drop_start',false); this.setState({drop_start: false}) Swal.close(); } }); } onClick_elButton = (ev) => { this.props.appActions.updateDataSlot('ds_commence_drop', "1"); } showOpenFileDlg = () => { this.inputOpenFileRef.current.click() } get_dir_path = (ev) => { console.log(ev.target.files[0]); } save_file = () => { console.log(this.state.poollog); console.log(this.state.file_name); } onDrop(event) { var fn = event.target.files[0].name; console.log(fn); this.setState({file_name: fn}); var file = event.target.files[0]; var reader = new FileReader(); reader.onload = (event,fn) => { this.setState({ poollog: event.target.result, }, () => {this.save_file()}); } reader.readAsText(file); } fname_change = (event) => { var fname = event.target.value; var valid_chars = /^[0-9a-zA-Z_]+$/; if(fname.match(valid_chars)) { this.setState({ file_name:fname, }); } } closeModal = () => { this.setState({isOpen:false}); } close_adrop = () => { this.setState({adrop_open:false}); } openModal = () => { this.setState({isOpen:true}); } // notify_tl = () => { notify_tl() { notification.error({ message: 'Function Currently Unavailable (Coming Soon)', duration: 10, description: 'We apologize for the inconvenience, but we are still trying to make the TronLink functionality as user-friendly as possible.', }); this.setState({ is_disabled: true, }) } render_state1() { // eslint-disable-next-line no-unused-vars let baseStyle = { height: '300px' }; // eslint-disable-next-line no-unused-vars let layoutFlowStyle = {}; const style_elBackground = { width: '100%', height: '100%', }; const style_elBackground_outer = { backgroundColor: '#f6f6f6', }; const style_elButton_outer = { position: 'absolute', textAlign: 'center', width: '50%', marginLeft: '25%', }; // const drop_disabled = (this.props.appActions.dataSlots.ds_login_method === 'tronlink') ? true:false; // var drop_btn; // if (drop_disabled) { // drop_btn = // <Button // style={{ // marginTop:'200px', // cursor: 'pointer', // pointerEvents: 'auto', // }} // block // disabled={this.state.is_disabled} // // onClick={() => {this.notify_tl();}} // onClick={this.run_airdrop} // // type="primary" // size='large'> // <Icon type="rocket" theme="filled" /> // <strong> Start Airdrop</strong> // </Button>; // } else { // drop_btn = // <Button // style={{ // marginTop:'200px', // cursor: 'pointer', // pointerEvents: 'auto', // }} // block // onClick={this.run_airdrop} // type="primary" // size='large'> // <Icon type="rocket" theme="filled" /> // <strong> Start Airdrop</strong> // </Button>; // } return ( <div className="Submitoptions" style={baseStyle}> <div className="background"> <div className='appBg containerMinHeight elBackground' style={style_elBackground_outer}> <div style={style_elBackground} /> </div> </div> <Modal title="Rename Export File" okText="Save" centered visible={this.state.isOpen} onOk={this.onClick_downloadBtn} onCancel={this.closeModal}> <Input value={this.state.file_name} addonAfter=".json" onChange={this.fname_change} /> </Modal> <div style={style_elButton_outer}> <Button style={{ marginTop:'60px', cursor: 'pointer', pointerEvents: 'auto', }} block onClick={this.openModal} // type="primary" size='large'> <Icon type="cloud-download" /> Download Data </Button> </div> <div style={style_elButton_outer}> <Button style={{ marginTop:'130px', cursor: 'pointer', pointerEvents: 'auto', }} block onClick={this.onClick_elButton} // type="primary" size='large'> <Icon type="file-search" /> View Data </Button> </div> <div style={style_elButton_outer}> <Button style={{ marginTop:'200px', cursor: 'pointer', pointerEvents: 'auto', }} block onClick={this.run_airdrop} type="primary" size='large'> <Icon type="rocket" theme="filled" /> <strong> Start Airdrop</strong> </Button> </div> </div> ) } notifyCopy = () => { message.success('Copied to clipboard!'); } render_state2() { // eslint-disable-next-line no-unused-vars const ds = this.props.appActions.dataSlots; const total_height = (ds.ds_login_method === 'tronlink') ? ((this.state.new_tl_acc !== null) ? '420px':'350px'):'300px'; let baseStyle = { height: total_height }; // eslint-disable-next-line no-unused-vars let layoutFlowStyle = {}; const style_elBackground = { width: '100%', height: '100%', }; const style_elBackground_outer = { backgroundColor: '#f6f6f6', }; const style_elButton_outer = { position: 'absolute', textAlign: 'center', marginTop: '220px', width: '80%', marginLeft: '10%' }; const top_text = { position: 'absolute', textAlign: 'center', marginTop: '60px', width: '60%', marginLeft: '20%', }; const note_style = { position: 'absolute', textAlign: 'center', marginTop: '275px', width: '95%', marginLeft: '2.5%' } const initial_style = { position: 'absolute', textAlign: 'center', marginTop: '290px', fontSize: 20, width: '95%', marginLeft: '2.5%' } const nr = ds.ds_airdrop_array.length; const s_num = ds.ds_success_list.length; const f_num = ds.ds_failure_list.length; const s_pct = Math.round(100*(s_num/nr)); const t_pct = Math.round(100*((s_num+f_num)/nr)); const s_txt = 'Successes: ' + s_num; const f_txt = 'Failures: ' + f_num; // var download_text = (ds.ds_login_method === 'tronlink') ? 'Download Results/Account Info':'Download Results'; var copy_text = null; var new_pk = ''; var new_addr = ''; if (ds.ds_login_method === 'tronlink') { if (this.state.new_tl_acc !== null) { new_pk = this.state.new_tl_acc.privateKey; new_addr = this.state.new_tl_acc.address.base58; copy_text = ( <div style={note_style}> <p> <strong> Below is the address and private key for the wallet performing the airdrop.<br /> Click either of these to copy them to the clipboard or click the "Download Results" button to save them to file. </strong> </p> <CopyToClipboard style={{cursor:'pointer',pointerEvents:'auto'}} text={new_addr} onCopy={this.notifyCopy}> <span>{new_addr}<br /></span> </CopyToClipboard> <CopyToClipboard style={{cursor:'pointer',pointerEvents:'auto'}} text={new_pk} onCopy={this.notifyCopy}> <span>{new_pk}</span> </CopyToClipboard> </div> ) } else { copy_text = ( <div style={initial_style}> <span><strong>Initializing airdrop...</strong></span> </div> ) } } return ( <div className="Submitoptions" style={baseStyle}> <div className="background"> <div className='appBg containerMinHeight elBackground' style={style_elBackground_outer}> <div style={style_elBackground} /> </div> </div> <Modal title="Rename Export File" okText="Save" centered visible={this.state.isOpen} onOk={this.onClick_saveAll} onCancel={this.closeModal}> <Input value={this.state.file_name} addonAfter=".json" onChange={this.fname_change} /> </Modal> <Row style={top_text}> <Col span={12}> <Progress percent={t_pct} successPercent={s_pct} type='dashboard' strokeColor='#ff6347' /> </Col> <Col span={12} style={{marginTop:'20px',fontSize:'16px'}}> <p> <strong style={{color:'#48d65e'}}>{s_txt}</strong> </p> <p> <strong style={{color:'#ff6347'}}>{f_txt}</strong> </p> </Col> </Row> <Row style={style_elButton_outer}> <Col span={12}> <Button style={{ cursor: 'pointer', pointerEvents: 'auto', width: '90%' }} block disabled={!this.state.is_complete} onClick={this.openModal} size='large'> <Icon type="cloud-download" /> Download Results </Button> </Col> <Col span={12}> <Button style={{ cursor: 'pointer', pointerEvents: 'auto', width: '90%' }} block disabled={!this.state.is_complete} onClick={() => {this.notify_complete();}} type="primary" size='large'> <Icon type="user-delete" /> <strong> Logout</strong> </Button> </Col> </Row> {copy_text} </div> ) } render() { if (!this.state.drop_start) {return this.render_state1()}; return this.render_state2(); } }