tronair-gui
Version:
Web application/GUI for performing airdrops on the TRON blockchain
855 lines (775 loc) • 26.2 kB
JavaScript
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();
}
}