@@ -15,7 +15,7 @@ public class Daemon | |||
public Daemon(String host) | |||
{ | |||
this.host = host + (host.endsWith("/") ? "" : "/") + "json_rpc"; | |||
this.host = (host.contains("://") ? "" : "http://") + host + (host.endsWith("/") ? "" : "/") + "json_rpc"; | |||
} | |||
public Daemon(String host, int port) | |||
@@ -11,10 +11,10 @@ import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import org.apache.logging.log4j.core.Logger; | |||
import org.apache.logging.log4j.core.LoggerContext; | |||
import org.json.JSONArray; | |||
import org.json.JSONObject; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import com.mashape.unirest.http.HttpResponse; | |||
import com.mashape.unirest.http.JsonNode; | |||
@@ -27,7 +27,7 @@ import fr.slixe.dero4j.util.MapBuilder; | |||
public class DeroWallet implements IWallet | |||
{ | |||
private static final Logger log = LoggerFactory.getLogger("Dero Wallet"); | |||
private static final Logger log = LoggerContext.getContext().getLogger("Dero Wallet"); | |||
private static final int SCALE = 12; | |||
@@ -44,7 +44,7 @@ public class DeroWallet implements IWallet | |||
private JSONObject request(JSONObject json) throws RequestException | |||
{ | |||
System.out.println(json); | |||
log.info(json.toString()); | |||
HttpResponse<JsonNode> req; | |||
try { | |||
req = Unirest.post(host).basicAuth(username, password).header("Content-Type", "application/json").body(json).asJson(); | |||
@@ -53,7 +53,7 @@ public class DeroWallet implements IWallet | |||
throw new RequestException("Wallet is offline?"); | |||
} | |||
JSONObject response = req.getBody().getObject(); | |||
System.out.println(response); | |||
log.info(response.toString()); | |||
if (!response.has("result")) { | |||
throw new RequestException(response.getJSONObject("error").getString("message")); | |||
} | |||
@@ -78,7 +78,11 @@ public class DeroWallet implements IWallet | |||
@Override | |||
public String transfer(String address, BigDecimal amount) throws RequestException | |||
{ | |||
JSONObject json = request(json("transfer", new MapBuilder<String, Object>().put("address", address).put("amount", Helper.asUint64(amount, SCALE)).get())); | |||
JSONObject dest = new JSONObject(); | |||
dest.put("address", address); | |||
dest.put("amount", Helper.asUint64(amount, SCALE)); | |||
JSONObject json = request(json("transfer", new MapBuilder<String, Object>().put("destinations", new JSONArray().put(dest)).get())); | |||
return json.getString("tx_hash"); | |||
} | |||
@@ -168,13 +172,19 @@ public class DeroWallet implements IWallet | |||
return null; | |||
} | |||
JSONObject result = json.getJSONObject("payments"); | |||
return new Tx.InPayment(result.getInt("block_height"), result.getString("tx_hash"), Helper.toBigDecimal(result.getBigInteger("amount"), SCALE), (byte) json.getInt("unlock_time"), json.getString("payment_id")); | |||
System.out.println("PAYMENTS: " + result.toString()); | |||
return new Tx.InPayment(result.getInt("block_height"), result.getString("tx_hash"), Helper.toBigDecimal(result.getBigInteger("amount"), SCALE), (byte) result.getInt("unlock_time"), result.getString("payment_id")); | |||
} | |||
@Override | |||
public BigDecimal estimateFee(String address, BigDecimal amount) throws RequestException | |||
{ | |||
JSONObject json = request(json("transfer", new MapBuilder<String, Object>().put("do_not_relay", true).put("address", address).put("amount", Helper.asUint64(amount, SCALE)).get())); | |||
JSONObject dest = new JSONObject(); | |||
dest.put("address", address); | |||
dest.put("amount", Helper.asUint64(amount, SCALE)); | |||
JSONObject json = request(json("transfer", new MapBuilder<String, Object>().put("do_not_relay", true).put("destinations", new JSONArray().put(dest)).get())); | |||
return Helper.toBigDecimal(json.getBigInteger("fee"), SCALE); | |||
} | |||
} |
@@ -15,11 +15,9 @@ import com.arangodb.ArangoCollection; | |||
import com.arangodb.ArangoCursor; | |||
import com.arangodb.ArangoDB; | |||
import com.arangodb.ArangoDatabase; | |||
import com.arangodb.entity.BaseDocument; | |||
import com.google.inject.Inject; | |||
import fr.slixe.dero4j.RequestException; | |||
import fr.slixe.dero4j.structure.Tx; | |||
import fr.slixe.dero4j.util.MapBuilder; | |||
@Singleton | |||
@@ -187,49 +185,32 @@ public class ArangoDatabaseService | |||
return all("FOR doc IN users RETURN doc", User.class, new HashMap<>()); | |||
} | |||
public void addTx(Tx tx, String userId) | |||
public void addTx(Transaction tx) | |||
{ | |||
BaseDocument base = new BaseDocument(); | |||
base.addAttribute("amount", tx.getAmount()); | |||
base.addAttribute("userId", userId); | |||
base.addAttribute("blockHeight", tx.getBlockHeight()); | |||
base.addAttribute("confirmed", false); | |||
base.addAttribute("confirmations", 0); | |||
base.setKey(tx.getTxHash()); | |||
txs.insertDocument(base); | |||
txs.insertDocument(tx); | |||
} | |||
public void updateTx(String txHash, int confirmations) | |||
{ | |||
BaseDocument doc = txs.getDocument(txHash, BaseDocument.class); | |||
doc.updateAttribute("confirmations", confirmations); | |||
Transaction doc = txs.getDocument(txHash, Transaction.class); | |||
if (confirmations == 20) | |||
doc.updateAttribute("confirmed", true); | |||
doc.setConfirmations(confirmations); | |||
txs.updateDocument(doc.getKey(), doc); | |||
txs.updateDocument(doc.getHash(), doc); | |||
} | |||
public void removeTx(String txHash) { | |||
txs.deleteDocument(txHash); | |||
} | |||
public List<BaseDocument> getConfirmedTxs() | |||
public List<Transaction> getConfirmedTxs() | |||
{ | |||
return getTxs(true); | |||
return all("FOR doc IN txs FILTER doc.confirmations == @confirmations RETURN doc", Transaction.class, new MapBuilder<String, Object>().put("confirmations", 20).get()); | |||
} | |||
public List<BaseDocument> getUnconfirmedTxs() | |||
public List<Transaction> getUnconfirmedTxs() | |||
{ | |||
return getTxs(false); | |||
} | |||
private List<BaseDocument> getTxs(boolean confirmed) | |||
{ | |||
return all("FOR doc IN txs FILTER doc.confirmed == @confirmed RETURN doc", BaseDocument.class, new MapBuilder<String, Object>().put("confirmed", confirmed).get()); | |||
return all("FOR doc IN txs FILTER doc.confirmations < 20 RETURN doc", Transaction.class, new MapBuilder<String, Object>().get()); | |||
} | |||
protected <T> T first(String query, Class<T> type, Map<String, Object> vars) | |||
@@ -259,4 +240,9 @@ public class ArangoDatabaseService | |||
return new User(userId, null, paymentId, BigDecimal.ZERO, BigDecimal.ZERO); | |||
} | |||
public boolean existTx(String txHash) | |||
{ | |||
return txs.documentExists(txHash); | |||
} | |||
} |
@@ -129,8 +129,8 @@ public class TipBot extends KrobotModule { | |||
this.daemon = new Daemon(daemonHost); | |||
timer.scheduleAtFixedRate(task, 0, TimeUnit.SECONDS.toMillis(30)); | |||
timer.scheduleAtFixedRate(verifyTask, 0, TimeUnit.SECONDS.toMillis(30)); | |||
timer.scheduleAtFixedRate(task, TimeUnit.SECONDS.toMillis(20), TimeUnit.SECONDS.toMillis(30)); | |||
timer.scheduleAtFixedRate(verifyTask, TimeUnit.SECONDS.toMillis(20), TimeUnit.SECONDS.toMillis(30)); | |||
} | |||
public void loadConfig() | |||
@@ -0,0 +1,76 @@ | |||
package fr.slixe.tipbot; | |||
import java.math.BigDecimal; | |||
import com.arangodb.entity.DocumentField; | |||
public class Transaction { | |||
@DocumentField(DocumentField.Type.KEY) | |||
private String hash; | |||
private String userId; | |||
private long blockHeight; | |||
private BigDecimal amount; | |||
private int confirmations; | |||
public Transaction() {} | |||
public Transaction(String hash, String userId, long blockHeight, BigDecimal amount) | |||
{ | |||
this.hash = hash; | |||
this.userId = userId; | |||
this.blockHeight = blockHeight; | |||
this.amount = amount; | |||
this.confirmations = 0; | |||
} | |||
public String getHash() | |||
{ | |||
return hash; | |||
} | |||
public void setHash(String hash) | |||
{ | |||
this.hash = hash; | |||
} | |||
public String getUserId() | |||
{ | |||
return userId; | |||
} | |||
public void setUserId(String userId) | |||
{ | |||
this.userId = userId; | |||
} | |||
public long getBlockHeight() | |||
{ | |||
return blockHeight; | |||
} | |||
public void setBlockHeight(long blockHeight) | |||
{ | |||
this.blockHeight = blockHeight; | |||
} | |||
public BigDecimal getAmount() | |||
{ | |||
return amount; | |||
} | |||
public void setAmount(BigDecimal amount) | |||
{ | |||
this.amount = amount; | |||
} | |||
public int getConfirmations() | |||
{ | |||
return confirmations; | |||
} | |||
public void setConfirmations(int confirmations) | |||
{ | |||
this.confirmations = confirmations; | |||
} | |||
} |
@@ -69,7 +69,7 @@ public class InfoCommand implements CommandHandler | |||
StringBuilder builder = new StringBuilder(); | |||
builder.append("Height / Topoheight: ").append(height + " / " + topoHeight).append("\n"); | |||
builder.append("Average Block Time: ").append(blockTime).append("\n"); | |||
builder.append("Average Block Time: ").append(blockTime).append("s").append("\n"); | |||
builder.append("Difficulty: ").append(difficulty).append("\n"); | |||
builder.append("Mempool: ").append(txMempool).append("\n"); | |||
builder.append("Total Supply: ").append(totalSupply).append("\n"); | |||
@@ -28,14 +28,14 @@ public class WithdrawCommand implements CommandHandler { | |||
@Override | |||
public Object handle(MessageContext ctx, ArgumentMap args) throws Exception { //TODO add Withdraw to a task and not call withdraw directly.. | |||
BigDecimal amount; | |||
MessageChannel chan = ctx.getChannel(); | |||
if (!(chan instanceof PrivateChannel)) | |||
{ | |||
chan = ctx.getUser().openPrivateChannel().complete(); | |||
} | |||
try { | |||
amount = new BigDecimal(args.get("amount", String.class)); | |||
if (amount.signum() != 1) | |||
@@ -45,15 +45,15 @@ public class WithdrawCommand implements CommandHandler { | |||
{ | |||
throw new CommandException(String.format(bot.getMessage("withdraw.err.invalid-amount"), ctx.getUser().getAsMention())); | |||
} | |||
String address = args.get("address"); | |||
String id = ctx.getUser().getId(); | |||
if (!wallet.hasEnoughFunds(id, amount)) | |||
{ | |||
throw new CommandException(bot.getMessage("withdraw.err.not-enough")); | |||
} | |||
BigDecimal fee; | |||
try { | |||
fee = wallet.getApi().estimateFee(address, amount); | |||
@@ -62,6 +62,7 @@ public class WithdrawCommand implements CommandHandler { | |||
e.printStackTrace(); | |||
throw new CommandException(e.getMessage()); | |||
} | |||
String tx; | |||
try { | |||
tx = wallet.getApi().transfer(address, amount); | |||
@@ -69,11 +70,11 @@ public class WithdrawCommand implements CommandHandler { | |||
e.printStackTrace(); | |||
throw new CommandException(bot.getMessage("withdraw.err.transfer")); | |||
} | |||
wallet.removeFunds(id, amount); | |||
chan.sendMessage(bot.dialog("Withdraw", String.format("You've withdrawn %s **DERO** to:\n%s\n\n__**Tx hash**__:\n%s\n\n__**Fee**__: %s", amount.subtract(fee), address, tx, fee))).queue(); | |||
return null; | |||
} | |||
} |
@@ -1,21 +1,20 @@ | |||
package fr.slixe.tipbot.task; | |||
import java.math.BigDecimal; | |||
import java.util.List; | |||
import java.util.TimerTask; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.apache.logging.log4j.core.Logger; | |||
import org.apache.logging.log4j.core.LoggerContext; | |||
import com.arangodb.entity.BaseDocument; | |||
import com.google.inject.Inject; | |||
import fr.slixe.dero4j.RequestException; | |||
import fr.slixe.tipbot.Transaction; | |||
import fr.slixe.tipbot.Wallet; | |||
public class VerifyTask extends TimerTask { | |||
private static final Logger log = LoggerFactory.getLogger("VerifyTask"); | |||
private static final Logger log = LoggerContext.getContext().getLogger("VerifyTask"); | |||
@Inject | |||
private Wallet wallet; | |||
@@ -32,30 +31,27 @@ public class VerifyTask extends TimerTask { | |||
log.error("Looks like wallet isn't reachable..."); | |||
return; | |||
} | |||
List<BaseDocument> docs = wallet.getDB().getUnconfirmedTxs(); | |||
for (BaseDocument doc : docs) | |||
List<Transaction> txs = wallet.getDB().getUnconfirmedTxs(); | |||
for (Transaction tx : txs) | |||
{ | |||
String txHash = doc.getKey(); | |||
String userId = (String) doc.getAttribute("userId"); | |||
int txBlockHeight = (int) doc.getAttribute("blockHeight"); | |||
BigDecimal amount = (BigDecimal) doc.getAttribute("amount"); | |||
int diff = blockHeight - txBlockHeight; | |||
int diff = (int) (blockHeight - tx.getBlockHeight()); | |||
diff = diff > 20 ? 20 : diff; | |||
try { | |||
if (diff == 20) //we wait 20 blocks to verify instead of veryfing every block | |||
{ | |||
if (!this.wallet.getApi().isValidTx(txHash)) | |||
if (!this.wallet.getApi().isValidTx(tx.getHash())) | |||
{ | |||
this.wallet.getDB().removeTx(txHash); | |||
log.error("Invalid transaction !! hash: '" + tx.getHash() + "'"); | |||
this.wallet.getDB().removeTx(tx.getHash()); | |||
continue; | |||
} | |||
else { | |||
this.wallet.addFunds(userId, amount); | |||
this.wallet.removeUnconfirmedFunds(userId, amount); | |||
log.info("Amount: " + tx.getAmount()); | |||
this.wallet.addFunds(tx.getUserId(), tx.getAmount()); | |||
this.wallet.removeUnconfirmedFunds(tx.getUserId(), tx.getAmount()); | |||
} | |||
} | |||
} catch (RequestException e) { | |||
@@ -63,7 +59,7 @@ public class VerifyTask extends TimerTask { | |||
continue; | |||
} | |||
this.wallet.getDB().updateTx(txHash, diff); | |||
this.wallet.getDB().updateTx(tx.getHash(), diff); | |||
} | |||
} | |||
} |
@@ -6,12 +6,13 @@ import java.util.TimerTask; | |||
import org.apache.logging.log4j.core.Logger; | |||
import org.apache.logging.log4j.core.LoggerContext; | |||
import org.krobot.Krobot; | |||
import org.krobot.util.Dialog; | |||
import com.google.inject.Inject; | |||
import fr.slixe.dero4j.RequestException; | |||
import fr.slixe.dero4j.structure.Tx; | |||
import fr.slixe.tipbot.TipBot; | |||
import fr.slixe.tipbot.Transaction; | |||
import fr.slixe.tipbot.User; | |||
import fr.slixe.tipbot.Wallet; | |||
@@ -22,6 +23,9 @@ public class WalletTask extends TimerTask | |||
@Inject | |||
private Wallet wallet; | |||
@Inject | |||
private TipBot bot; | |||
private int lastBlockHeight = 0; | |||
public WalletTask() {} | |||
@@ -45,37 +49,52 @@ public class WalletTask extends TimerTask | |||
{ | |||
log.info("skip this execution, block height is the same."); | |||
return; | |||
} else if ((diff = blockHeight - this.lastBlockHeight) > 1) | |||
} else if (blockHeight - this.lastBlockHeight > 1) | |||
{ | |||
log.warn("Multiple blocks detected, current block height is " + blockHeight); | |||
diff = blockHeight - this.lastBlockHeight; | |||
log.warn("Multiple blocks detected, current wallet block height is " + blockHeight + " and last block height is " + this.lastBlockHeight); | |||
blockHeight = blockHeight - diff + 1; | |||
log.warn("Now, it's " + blockHeight + " with a diff at " + diff); | |||
log.warn("Now, it's " + blockHeight); | |||
} | |||
if (blockHeight < 0) | |||
blockHeight = 0; | |||
final List<User> users = this.wallet.getDB().getUsers(); | |||
for (User doc : users) { | |||
final String userId = doc.getKey(); //using userId as key | |||
final String paymentId = (String) doc.getPaymentId(); | |||
if (paymentId == null) continue; | |||
final List<Tx> transactions; | |||
try { | |||
log.info("Fetch bulk payments with blockHeight " + blockHeight); | |||
transactions = this.wallet.getApi().getTransactions(paymentId, blockHeight); | |||
} catch (RequestException e) { | |||
e.printStackTrace(); | |||
continue; | |||
} | |||
Krobot.getRuntime().jda().getUserById(userId).openPrivateChannel().queue((e) -> { | |||
log.info("Private channel opened with " + e.getUser().getName()); | |||
for (Tx tx : transactions) { | |||
log.info("New incoming transaction for user " + userId); | |||
if (this.wallet.getDB().existTx(tx.getTxHash())) | |||
{ | |||
log.error(String.format("Duplicated TX Hash: '%s', ignored...", tx.getTxHash())); | |||
continue; | |||
} | |||
this.wallet.addUnconfirmedFunds(userId, tx.getAmount()); | |||
this.wallet.getDB().addTx(tx, userId); | |||
this.wallet.getDB().addTx(new Transaction(tx.getTxHash(), userId, tx.getBlockHeight(), tx.getAmount())); | |||
e.sendMessage(Dialog.info("Deposit", String.format("__**Amount**__: %s\n__**Tx hash**__: %s\n__**Block height**__: %s", tx.getAmount(), tx.getTxHash(), tx.getBlockHeight()))).queue(); | |||
e.sendMessage(bot.dialog("Deposit", String.format("__**Amount**__: %s\n__**Tx hash**__: %s\n__**Block height**__: %s", tx.getAmount(), tx.getTxHash(), tx.getBlockHeight()))).queue(); | |||
} | |||
}); | |||
} | |||