Browse Source

initial commit

master
Slixe 1 year ago
commit
a29bb6e3fa
32 changed files with 2195 additions and 0 deletions
  1. +2
    -0
      bin/.gitignore
  2. +67
    -0
      build.gradle
  3. BIN
      dero_linux_amd64/dero-wallet-cli-linux-amd64
  4. BIN
      gradle/wrapper/gradle-wrapper.jar
  5. +5
    -0
      gradle/wrapper/gradle-wrapper.properties
  6. +172
    -0
      gradlew
  7. +84
    -0
      gradlew.bat
  8. +1
    -0
      krobot
  9. +11
    -0
      settings.gradle
  10. +52
    -0
      src/main/java/fr/slixe/dero4j/Daemon.java
  11. +180
    -0
      src/main/java/fr/slixe/dero4j/DeroWallet.java
  12. +99
    -0
      src/main/java/fr/slixe/dero4j/IWallet.java
  13. +9
    -0
      src/main/java/fr/slixe/dero4j/RequestException.java
  14. +52
    -0
      src/main/java/fr/slixe/dero4j/structure/SmartContract.java
  15. +90
    -0
      src/main/java/fr/slixe/dero4j/structure/Tx.java
  16. +35
    -0
      src/main/java/fr/slixe/dero4j/util/Helper.java
  17. +21
    -0
      src/main/java/fr/slixe/dero4j/util/MapBuilder.java
  18. +262
    -0
      src/main/java/fr/slixe/tipbot/ArangoDatabaseService.java
  19. +169
    -0
      src/main/java/fr/slixe/tipbot/TipBot.java
  20. +87
    -0
      src/main/java/fr/slixe/tipbot/User.java
  21. +154
    -0
      src/main/java/fr/slixe/tipbot/Wallet.java
  22. +47
    -0
      src/main/java/fr/slixe/tipbot/command/BalanceCommand.java
  23. +9
    -0
      src/main/java/fr/slixe/tipbot/command/CommandException.java
  24. +151
    -0
      src/main/java/fr/slixe/tipbot/command/HelpCommand.java
  25. +82
    -0
      src/main/java/fr/slixe/tipbot/command/InfoCommand.java
  26. +71
    -0
      src/main/java/fr/slixe/tipbot/command/TipCommand.java
  27. +79
    -0
      src/main/java/fr/slixe/tipbot/command/WithdrawCommand.java
  28. +69
    -0
      src/main/java/fr/slixe/tipbot/task/VerifyTask.java
  29. +85
    -0
      src/main/java/fr/slixe/tipbot/task/WalletTask.java
  30. +7
    -0
      src/main/resources/defaults/arango.default.json
  31. +32
    -0
      src/main/resources/defaults/general.default.json
  32. +11
    -0
      src/main/resources/defaults/wallet.default.json

+ 2
- 0
bin/.gitignore View File

@@ -0,0 +1,2 @@
/default/
/main/

+ 67
- 0
build.gradle View File

@@ -0,0 +1,67 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java Library project to get you started.
* For more details take a look at the Java Libraries chapter in the Gradle
* user guide available at https://docs.gradle.org/5.0/userguide/java_library_plugin.html
*/
plugins {
id 'java-library'
id 'application'
id 'eclipse'
}
compileJava.options.encoding = 'UTF-8'
mainClassName = 'fr.slixe.tipbot.TipBot'
group 'fr.slixe'
version '1.0.0'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
jcenter()
mavenCentral()
maven {
url 'http://krobot-framework.github.io/maven'
}
maven {
url 'http://wytrem.github.io/maven'
}
}
dependencies {
implementation 'com.google.guava:guava:26.0-jre'
//implementation 'org.krobot:krobot-framework:3.0.0-ALPHA-09'
implementation project(":krobot")
implementation 'com.mashape.unirest:unirest-java:1.4.9'
implementation 'com.arangodb:arangodb-java-driver:5.0.0'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'net.dv8tion:JDA:3.8.3_460'
implementation 'com.google.inject:guice:4.2.2'
implementation 'org.apache.logging.log4j:log4j-api:2.11.2'
implementation 'org.apache.logging.log4j:log4j-core:2.11.2'
}
task fatJar(type: Jar) {
from {
configurations
.runtimeClasspath
.findAll { !it.name.endsWith('pom') }
.collect { it.isDirectory() ? it : zipTree(it) }
}
with jar
baseName = project.name + '-all'
manifest {
attributes 'Main-Class': mainClassName
}
}

BIN
dero_linux_amd64/dero-wallet-cli-linux-amd64 View File


BIN
gradle/wrapper/gradle-wrapper.jar View File


+ 5
- 0
gradle/wrapper/gradle-wrapper.properties View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

+ 172
- 0
gradlew View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh

##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn () {
echo "$*"
}

die () {
echo
echo "$*"
echo
exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option

if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi

# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi

exec "$JAVACMD" "$@"

+ 84
- 0
gradlew.bat View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

+ 1
- 0
krobot

@@ -0,0 +1 @@
Subproject commit 802cd068c25dbfeb5ca8cf28b25e01de5f2c2f88

+ 11
- 0
settings.gradle View File

@@ -0,0 +1,11 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user guide at https://docs.gradle.org/5.0/userguide/multi_project_builds.html
*/
rootProject.name = 'DERO - TipBot'
include 'krobot'

+ 52
- 0
src/main/java/fr/slixe/dero4j/Daemon.java View File

@@ -0,0 +1,52 @@
package fr.slixe.dero4j;
import org.json.JSONObject;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import fr.slixe.dero4j.util.Helper;
public class Daemon
{
private final String host;
public Daemon(String host, int port)
{
this.host = String.format("http://%s:%d/json_rpc", host, port);
}
private JSONObject post(JSONObject json) throws RequestException
{
HttpResponse<JsonNode> httpResponse;
try {
httpResponse = Unirest.post(host).header("Content-Type", "application/json").body(json).asJson();
} catch (UnirestException e) {
e.printStackTrace();
throw new RequestException("Daemon isn't available");
}
JSONObject response = httpResponse.getBody().getObject();
if (!response.has("result"))
throw new RequestException(response.getString("error"));
return response.getJSONObject("result");
}
public int getHeight() throws RequestException
{
return post(Helper.json("getheight")).getInt("height");
}
public JSONObject getInfo() throws RequestException
{
return post(Helper.json("get_info"));
}
public String getHost()
{
return this.host;
}
}

+ 180
- 0
src/main/java/fr/slixe/dero4j/DeroWallet.java View File

@@ -0,0 +1,180 @@
package fr.slixe.dero4j;
import static fr.slixe.dero4j.util.Helper.asUint64;
import static fr.slixe.dero4j.util.Helper.json;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import fr.slixe.dero4j.structure.Tx;
import fr.slixe.dero4j.util.Helper;
import fr.slixe.dero4j.util.MapBuilder;
public class DeroWallet implements IWallet
{
private static final Logger log = LoggerFactory.getLogger("Dero Wallet");
private static final int SCALE = 12;
private final String host;
private final String username;
private final String password;
public DeroWallet(String host, int port, String username, String password)
{
this.host = String.format("http://%s:%d/json_rpc", host, port);
this.username = username;
this.password = password;
}
private JSONObject request(JSONObject json) throws RequestException
{
System.out.println(json);
HttpResponse<JsonNode> req;
try {
req = Unirest.post(host).basicAuth(username, password).header("Content-Type", "application/json").body(json).asJson();
} catch (UnirestException e) {
log.error("Wallet is offline or invalid host/username/password ?");
throw new RequestException("Wallet is offline?");
}
JSONObject response = req.getBody().getObject();
System.out.println(response);
if (!response.has("result")) {
throw new RequestException(response.getJSONObject("error").getString("message"));
}
return response.getJSONObject("result");
}
@Override
public String generateAddress(String id) throws RequestException
{
JSONObject json = request(json("make_integrated_address", new MapBuilder<String, Object>().put("payment_id", id).get()));
return json.getString("integrated_address"); //make_integrated_address
}
@Override
public String getPaymentIdFromAddress(String address) throws RequestException
{
JSONObject json = request(json("split_integrated_address", new MapBuilder<String, Object>().put("integrated_address", address).get()));
return json.getString("payment_id"); //split_integrated_address
}
@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()));
return json.getString("tx_hash");
}
@Override
public String transferSplit(String address, BigDecimal amount) throws RequestException
{
throw new RequestException("Not implemented yet");
//JSONObject json = request(json("transfer_split", new MapBuilder<String, Object>().put("address", address).put("amount", Helper.asUint64(amount, SCALE)).get()));
//return json.getString("tx_hash");
}
@Override
public List<Tx> getTransactions(String id, int minHeight) throws RequestException
{
List<Tx> list = new ArrayList<>();
JSONObject json = request(json("get_bulk_payments", new MapBuilder<String, Object>().put("min_block_height", minHeight).put("payment_ids", new String[]{id}).get()));
if (!json.has("payments")) return list;
JSONArray array = json.getJSONArray("payments");
Iterator<Object> it = array.iterator();
while (it.hasNext()) {
JSONObject j = (JSONObject) it.next();
int blockHeight = j.getInt("block_height");
String txHash = j.getString("tx_hash");
BigDecimal amount = Helper.toBigDecimal(j.getBigInteger("amount"), SCALE);
byte lockedTime = (byte) j.getInt("unlock_time");
list.add(new Tx.In(blockHeight, txHash, amount, lockedTime, null)); //fromAddress is null because it's isn't returned in json response
}
return list;
}
@Override
public String getPrimaryAddress() throws RequestException
{
return request(json("getaddress")).getString("address"); //getaddress
}
@Override
public int getHeight() throws RequestException
{
return request(json("getheight")).getInt("height"); //getheight
}
@Override
public String sendToSC(String scid, String entrypoint, Map<String, Object> scParams) throws RequestException
{
return sendToSC(scid, entrypoint, scParams, null);
}
@Override
public String sendToSC(String scid, String entrypoint, Map<String, Object> scParams, BigDecimal amount) throws RequestException
{
LinkedHashMap<String, Object> params = new MapBuilder<String, Object>().put("get_tx_key", true).get();
LinkedHashMap<String, Object> scTx = new MapBuilder<String, Object>().put("scid", scid).put("entrypoint", entrypoint).get();
if (amount != null)
params.put("value", asUint64(amount, 12));
if (scParams != null)
scTx.put("params", scParams);
params.put("sc_tx", scTx);
return request(json("transfer_split", params)).toString(); //TODO
}
@Override
public String paymentId()
{
StringBuilder builder = new StringBuilder(64);
SecureRandom rnd = new SecureRandom();
while (builder.length() < 64)
builder.append(Integer.toHexString(rnd.nextInt()));
return builder.substring(0, 64);
}
@Override
public boolean isValidTx(String txHash) throws RequestException
{
Tx.InPayment tx = getTransferByHash(txHash);
return tx != null && (tx.getBlockHeight() + 20) <= getHeight();
}
@Override
public Tx.InPayment getTransferByHash(String txHash) throws RequestException
{
JSONObject json = request(json("get_transfer_by_txid", new MapBuilder<String, Object>().put("txid", txHash).get()));
if (!json.has("payments")) {
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"));
}
@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()));
return Helper.toBigDecimal(json.getBigInteger("fee"), SCALE);
}
}

+ 99
- 0
src/main/java/fr/slixe/dero4j/IWallet.java View File

@@ -0,0 +1,99 @@
package fr.slixe.dero4j;
import fr.slixe.dero4j.structure.Tx;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
public interface IWallet
{
/**
* generate a new integrated address with the payment id
* @param id which represent paymentId
* @return new integrated address
* @throws RequestException
*/
String generateAddress(String id) throws RequestException;
/**
* @param address (integrated address)
* @return paymentId in this integrated address
* @throws RequestException
*/
String getPaymentIdFromAddress(String address) throws RequestException;
/**
* @param address is the receiver address
* @param amount amount to sent
* @return
* @throws RequestException
*/
String transfer(String address, BigDecimal amount) throws RequestException;
/**
* @param address is the receiver address
* @param amount amount to sent
* @return
* @throws RequestException
*/
String transferSplit(String address, BigDecimal amount) throws RequestException;
/**
* @param paymentId
* @param minHeight minimum height to start to count transactions
* @return list of incoming transactions
* @throws RequestException
*/
List<Tx> getTransactions(String paymentId, int minHeight) throws RequestException;
/**
* @return the primary address of this wallet
* @throws RequestException
*/
String getPrimaryAddress() throws RequestException;
/**
* @return current block height of wallet
* @throws RequestException
*/
int getHeight() throws RequestException;
/**
* @param scid smart contract id
* @param entrypoint function in SC
* @param scParams are the parameters used by smart contract. Map can be null
* @param amount to send to scid
* @return tx hash
* @throws RequestException
*/
String sendToSC(String scid, String entrypoint, Map<String, Object> scParams, BigDecimal amount) throws RequestException;
/**
* @param scid smart contract id
* @param entrypoint function in SC
* @param scParams are the parameters used by smart contract. Map can be null
* @return tx hash
* @throws RequestException
*/
String sendToSC(String scid, String entrypoint, Map<String, Object> scParams) throws RequestException;
/**
* Generate a new 64 hex bytes paymentId
* @return a new generated paymentId
*/
String paymentId();
/**
* The transaction is validated if it exists and it is confirmed up to 20 block minimum
* @param txHash
* @return txHash is valid or not
*/
boolean isValidTx(String txHash) throws RequestException;
/**
* Recover the transaction using tx hash
* @param txHash
* @return the transaction
*/
Tx.InPayment getTransferByHash(String txHash) throws RequestException;
/**
* Estimate fee before sending DERO
* @param address receiver
* @param amount to send
* @return estimated fee for this tx
*/
BigDecimal estimateFee(String address, BigDecimal amount) throws RequestException;
}

+ 9
- 0
src/main/java/fr/slixe/dero4j/RequestException.java View File

@@ -0,0 +1,9 @@
package fr.slixe.dero4j;
public class RequestException extends Exception
{
public RequestException(String message)
{
super(message);
}
}

+ 52
- 0
src/main/java/fr/slixe/dero4j/structure/SmartContract.java View File

@@ -0,0 +1,52 @@
package fr.slixe.dero4j.structure;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import fr.slixe.dero4j.Daemon;
import fr.slixe.dero4j.RequestException;
import org.json.JSONObject;
public class SmartContract
{
private final String url;
private final String scid;
public SmartContract(Daemon daemon, String scid)
{
this.url = daemon.getHost() + "/gettransactions";
this.scid = scid;
}
public String[] query(boolean balance, String... key) throws RequestException
{
HttpResponse<JsonNode> httpResponse;
try {
httpResponse = Unirest.post(this.url).header("Content-Type", "application/json").body(new JSONObject().put("txs_hashes", new String[]{scid}).put("sc_keys", key)).asJson();
} catch (UnirestException e) {
throw new RequestException("Daemon isn't reachable.");
}
JSONObject json = httpResponse.getBody().getObject();
if (json.getString("status").equals("TX NOT FOUND"))
throw new RequestException("This SCID doesn't exist!");
String[] array = new String[key.length];
JSONObject scKeys = json.getJSONArray("txs").getJSONObject(0).getJSONObject("sc_keys");
for (int i = 0; i < array.length; i++)
{
if (!scKeys.has(key[i]) || scKeys.isNull(key[i]))
array[i] = null;
else
array[i] = scKeys.getString(key[i]);
}
return array;
}
public String getID()
{
return this.scid;
}
}

+ 90
- 0
src/main/java/fr/slixe/dero4j/structure/Tx.java View File

@@ -0,0 +1,90 @@
package fr.slixe.dero4j.structure;
import java.math.BigDecimal;
public class Tx
{
private int blockHeight;
private String txHash;
private BigDecimal amount;
private byte unlockTime;
protected Tx(int blockHeight, String txHash, BigDecimal amount, byte unlockTime)
{
this.blockHeight = blockHeight;
this.txHash = txHash;
this.amount = amount;
this.unlockTime = unlockTime;
}
public int getBlockHeight()
{
return blockHeight;
}
public String getTxHash()
{
return txHash;
}
public BigDecimal getAmount()
{
return amount;
}
public byte getUnlockTime()
{
return unlockTime;
}
public static class In extends Tx {
private String fromAddress;
public In(int blockHeight, String txHash, BigDecimal amount, byte unlockTime, String fromAddress)
{
super(blockHeight, txHash, amount, unlockTime);
this.fromAddress = fromAddress;
}
public String getFromAddress()
{
return fromAddress;
}
}
public static class InPayment extends Tx {
private String paymentId;
public InPayment(int blockHeight, String txHash, BigDecimal amount, byte unlockTime, String paymentId)
{
super(blockHeight, txHash, amount, unlockTime);
this.paymentId = paymentId;
}
public String getPaymentId()
{
return this.paymentId;
}
}
public static class Out extends Tx {
private String toAddress;
public Out(int blockHeight, String txHash, BigDecimal amount, byte unlockTime, String toAddress)
{
super(blockHeight, txHash, amount, unlockTime);
this.toAddress = toAddress;
}
public String getToAddress()
{
return toAddress;
}
}
}

+ 35
- 0
src/main/java/fr/slixe/dero4j/util/Helper.java View File

@@ -0,0 +1,35 @@
package fr.slixe.dero4j.util;
import org.json.JSONObject;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Map;
public class Helper
{
public static JSONObject json(String method)
{
return json(method, null);
}
public static JSONObject json(String method, Map<String, Object> params)
{
JSONObject json = new JSONObject().put("jsonrpc", "2.0").put("id", "1").put("method", method);
if (params != null) json.put("params", params);
return json;
}
public static BigInteger asUint64(BigDecimal value, int scale)
{
return value.scaleByPowerOfTen(scale).toBigIntegerExact();
}
public static BigDecimal toBigDecimal(BigInteger value, int scale)
{
BigDecimal bigDecimal = new BigDecimal(Math.pow(10, scale));
return new BigDecimal(value).divide(bigDecimal).setScale(scale, RoundingMode.HALF_DOWN);
}
}

+ 21
- 0
src/main/java/fr/slixe/dero4j/util/MapBuilder.java View File

@@ -0,0 +1,21 @@
package fr.slixe.dero4j.util;
import java.util.LinkedHashMap;
public class MapBuilder<K, V>
{
private final LinkedHashMap<K, V> linkedHashMap = new LinkedHashMap<>();
public MapBuilder() {}
public MapBuilder<K, V> put(K key, V value)
{
this.linkedHashMap.put(key, value);
return this;
}
public LinkedHashMap<K, V> get()
{
return this.linkedHashMap;
}
}

+ 262
- 0
src/main/java/fr/slixe/tipbot/ArangoDatabaseService.java View File

@@ -0,0 +1,262 @@
package fr.slixe.tipbot;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Singleton;
import org.krobot.config.ConfigProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
public class ArangoDatabaseService
{
private static final Logger log = LoggerFactory.getLogger("Arango");
@Inject
private ConfigProvider config;
@Inject
private Wallet wallet;
private ArangoDB arango;
private ArangoDatabase db;
private ArangoCollection users;
private ArangoCollection txs;
public ArangoDatabaseService()
{}
public void start()
{
String host = this.config.at("arango.host");
int port = this.config.at("arango.port", int.class);
String user = this.config.at("arango.username");
String password = this.config.at("arango.password");
String database = this.config.at("arango.database");
log.info("Connecting to ArangoDB at {}:{}...", host, port);
this.arango = new ArangoDB.Builder()
.host(host, port)
.user(user)
.password(password)
.build();
this.db = this.arango.db(database);
try {
if (!this.db.exists()) {
this.db.create();
}
}
catch (Exception ignored)
{
System.err.println("Couldn't connect to ArangoDB! Please verify your config file.");
System.exit(1);
}
this.users = this.db.collection("users");
if (!this.users.exists()) {
this.users.create();
}
this.txs = this.db.collection("txs");
if (!this.txs.exists()) {
this.txs.create();
}
log.info("Connected to ArangoDB");
}
public void setBalance(String key, BigDecimal amount)
{
User doc = users.getDocument(key, User.class);
if (doc == null) {
doc = createUser(key);
users.insertDocument(doc);
}
doc.setBalance(amount);
users.updateDocument(doc.getKey(), doc);
}
public BigDecimal getBalance(String userId)
{
User doc = users.getDocument(userId, User.class);
if (doc == null) {
doc = createUser(userId);
users.insertDocument(doc);
}
return doc.getBalance();
}
public void setUnconfirmedBalance(String key, BigDecimal amount)
{
User doc = users.getDocument(key, User.class);
if (doc == null)
{
doc = createUser(key);
users.insertDocument(doc);
}
doc.setUnconfirmedBalance(amount);
users.updateDocument(doc.getKey(), doc);
}
public BigDecimal getUnconfirmedBalance(String userId)
{
User doc = users.getDocument(userId, User.class);
if (doc == null) {
doc = createUser(userId);
users.insertDocument(doc);
}
return doc.getUnconfirmedBalance();
}
public String getAddress(String key)
{
User doc = users.getDocument(key, User.class);
if (doc == null) {
doc = createUser(key);
users.insertDocument(doc);
}
return doc.getAddress();
}
public void setAddress(String key, String address)
{
User doc = users.getDocument(key, User.class);
if (doc == null) {
doc = createUser(key);
users.insertDocument(doc);
}
doc.setAddress(address);
users.updateDocument(doc.getKey(), doc);
}
public String getPaymentId(String userId)
{
User doc = users.getDocument(userId, User.class);
if (doc == null) {
doc = createUser(userId);
users.insertDocument(doc);
}
return doc.getPaymentId();
}
public void setPaymentId(String key, String paymentId)
{
User doc = users.getDocument(key, User.class);
if (doc == null) {
doc = createUser(key);
users.insertDocument(doc);
}
doc.setPaymentId(paymentId);
users.updateDocument(doc.getKey(), doc);
}
public List<User> getUsers()
{
return all("FOR doc IN users RETURN doc", User.class, new HashMap<>());
}
public void addTx(Tx tx, String userId)
{
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);
}
public void updateTx(String txHash, int confirmations)
{
BaseDocument doc = txs.getDocument(txHash, BaseDocument.class);
doc.updateAttribute("confirmations", confirmations);
if (confirmations == 20)
doc.updateAttribute("confirmed", true);
txs.updateDocument(doc.getKey(), doc);
}
public void removeTx(String txHash) {
txs.deleteDocument(txHash);
}
public List<BaseDocument> getConfirmedTxs()
{
return getTxs(true);
}
public List<BaseDocument> 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());
}
protected <T> T first(String query, Class<T> type, Map<String, Object> vars)
{
ArangoCursor<T> cursor = db.query(query, vars, null, type);
if (cursor.hasNext()) {
return cursor.next();
}
return null;
}
protected <T> List<T> all(String query, Class<T> type, Map<String, Object> vars)
{
return db.query(query, vars, null, type).asListRemaining();
}
protected User createUser(String userId)
{
String paymentId = wallet.getApi().paymentId();
try {
return new User(userId, wallet.getApi().generateAddress(paymentId), paymentId, BigDecimal.ZERO, BigDecimal.ZERO);
} catch (RequestException e) {
e.printStackTrace();
}
return new User(userId, null, paymentId, BigDecimal.ZERO, BigDecimal.ZERO);
}
}

+ 169
- 0
src/main/java/fr/slixe/tipbot/TipBot.java View File

@@ -0,0 +1,169 @@
package fr.slixe.tipbot;
import java.awt.Color;
import java.math.BigDecimal;
import java.util.Timer;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.krobot.Krobot;
import org.krobot.KrobotModule;
import org.krobot.command.CommandFilter;
import org.krobot.command.ExceptionHandler;
import org.krobot.config.ConfigProvider;
import org.krobot.module.Include;
import org.krobot.util.Dialog;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import fr.slixe.dero4j.Daemon;
import fr.slixe.tipbot.command.BalanceCommand;
import fr.slixe.tipbot.command.CommandException;
import fr.slixe.tipbot.command.HelpCommand;
import fr.slixe.tipbot.command.InfoCommand;
import fr.slixe.tipbot.command.TipCommand;
import fr.slixe.tipbot.command.WithdrawCommand;
import fr.slixe.tipbot.task.VerifyTask;
import fr.slixe.tipbot.task.WalletTask;
import net.dv8tion.jda.core.entities.MessageEmbed;
import net.dv8tion.jda.core.entities.PrivateChannel;
@Singleton
@Include(commands = { TipCommand.class, BalanceCommand.class, WithdrawCommand.class, InfoCommand.class,
HelpCommand.class })
@org.krobot.Bot(author = "Slixe", name = "Dero TipBot", version = "0.0.1")
public class TipBot extends KrobotModule {
private static final Logger log = LoggerContext.getContext().getLogger("TipBot");
private Timer timer = new Timer();
private Daemon daemon;
private String iconUrl;
private String roleId;
private Color customColor;
@Inject
private WalletTask task;
@Inject
private VerifyTask verifyTask;
@Inject
private ArangoDatabaseService db;
@Inject
private Wallet wallet;
@Inject
private ConfigProvider config;
@Inject
private ExceptionHandler exceptionHandler;
@Override
public void preInit() {
folder("config/").configs("arango").withDefaultsIn().classpathFolder("/defaults/");
folder("config/").configs("wallet").withDefaultsIn().classpathFolder("/defaults/");
folder("config/").configs("general").withDefaultsIn().classpathFolder("/defaults/");
}
@Override
public void init() {
prefix("/");
CommandFilter roleFilter = (cmd, context, args) -> { //Role filter
if (context.getChannel() instanceof PrivateChannel || !context.getMember().getRoles().contains(context.getGuild().getRoleById(this.roleId)))
cmd.setCancelled(true);
};
command("give", (context, map) -> { //only for debug
wallet.addFunds(context.getUser().getId(), new BigDecimal("9.5"));
return null;
}).filter(roleFilter);
command("shutdown-wallet", (context, map) -> {
if (this.wallet.getProcess() != null)
this.wallet.getProcess().destroyForcibly();
return dialog("Wallet shutdown", "The DERO wallet is now stopped.");
}).filter(roleFilter);
command("shutdown-bot", (context, map) -> {
log.warn("Shutdown requested !!");
if (this.wallet.getProcess() != null)
{
log.warn("destroying wallet process...");
this.wallet.getProcess().destroyForcibly();
log.warn("Wallet process should be destroyed.");
}
context.send(dialog("Shutdown", "DERO Tip Bot will be offline after this message.")).get(); //wait for it
log.warn("Exiting BOT...");
System.exit(0);
return null;
}).filter(roleFilter);
this.exceptionHandler.on(CommandException.class, (context, t) -> context.send(Dialog.error("Error !", t.getMessage())));
loadConfig();
log.info("Dero TipBot is now running!");
}
@Override
public void postInit() {
db.start();
wallet.start();
String daemonHost = this.config.at("wallet.daemon");
String[] daemonSplit = daemonHost.split(":");
String daemonAddress = daemonSplit[0];
int daemonPort = Integer.parseInt(daemonSplit[1]);
this.daemon = new Daemon(daemonAddress, daemonPort);
timer.scheduleAtFixedRate(task, 0, TimeUnit.SECONDS.toMillis(30));
timer.scheduleAtFixedRate(verifyTask, 0, TimeUnit.SECONDS.toMillis(30));
}
public void loadConfig()
{
this.iconUrl = this.config.at("general.iconUrl");
this.customColor = Color.decode(this.config.at("general.customColor"));
this.roleId = this.config.at("general.roleId");
if (this.roleId.isEmpty())
{
log.error("RoleId isn't filled in general.json !! Aborting...");
System.exit(0);
}
}
public String getMessage(String key)
{
return this.config.at("general." + key);
}
public MessageEmbed dialog(String title, String description)
{
return Dialog.dialog(this.customColor, title, description, this.iconUrl);
}
public Daemon getDaemon()
{
return daemon;
}
public static void main(String[] args) throws Exception {
Krobot.create().readTokenFromArgs(args).saveTokenIn(".token").run(TipBot.class);
}
}

+ 87
- 0
src/main/java/fr/slixe/tipbot/User.java View File

@@ -0,0 +1,87 @@
package fr.slixe.tipbot;
import com.arangodb.entity.DocumentField;
import java.math.BigDecimal;
public class User
{
@DocumentField(DocumentField.Type.KEY)
private String key;
private String address;
private String paymentId;
private BigDecimal balance;
private BigDecimal unconfirmedBalance;
public User() {}
public User(String userId, String address, String paymentId, BigDecimal balance, BigDecimal unconfirmedBalance)
{
this.key = userId;
this.address = address;
this.paymentId = paymentId;
this.balance = balance;
this.unconfirmedBalance = unconfirmedBalance;
}
public String getKey()
{
return key;
}
public void setKey(String key)
{
this.key = key;
}
/*
public String getUserId()
{
return userId;
}
public void setUserId(String userId)
{
this.userId = userId;
}
*/
public String getAddress()
{
return address;
}
public void setAddress(String address)
{
this.address = address;
}
public String getPaymentId()
{
return paymentId;
}
public void setPaymentId(String paymentId)
{
this.paymentId = paymentId;
}
public BigDecimal getBalance()
{
return balance;
}
public void setBalance(BigDecimal balance)
{
this.balance = balance;
}
public BigDecimal getUnconfirmedBalance()
{
return unconfirmedBalance;
}
public void setUnconfirmedBalance(BigDecimal balance)
{
this.unconfirmedBalance = balance;
}
}

+ 154
- 0
src/main/java/fr/slixe/tipbot/Wallet.java View File

@@ -0,0 +1,154 @@
package fr.slixe.tipbot;
import java.io.File;
import java.lang.ProcessBuilder.Redirect;
import java.math.BigDecimal;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.krobot.config.ConfigProvider;
import fr.slixe.dero4j.DeroWallet;
import fr.slixe.dero4j.RequestException;
@Singleton
public class Wallet {
private static final Logger log = LoggerContext.getContext().getLogger("Wallet");
@Inject
private ArangoDatabaseService db;
@Inject
private ConfigProvider config;
private DeroWallet api;
private Thread thread;
private volatile Process process;
public Wallet() {}
public void start()
{
String host = this.config.at("wallet.host");
int port = this.config.at("wallet.port", int.class);
String username = this.config.at("wallet.username");
String password = this.config.at("wallet.password");
String daemon = this.config.at("wallet.daemon");
boolean autoLaunch = this.config.at("wallet.autoLaunch", boolean.class);
String walletPath = this.config.at("wallet.walletFilePath");
String walletPassword = this.config.at("wallet.walletPassword");
if (autoLaunch)
{
log.info("Trying to start DERO Wallet...");
String path = this.config.at("wallet.launchPath");
File walletFile = new File(path);
if (!walletFile.exists())
{
log.error(String.format("Error! Please verify your config, wallet file not found at '%s'.", path));
System.exit(1);
}
this.thread = new Thread(() -> {
try {
String cmd[] = {path, "--testnet", "--rpc-server", String.format("--rpc-bind=%s:%d", host, port), String.format("--rpc-login=%s:%s", username, password),
String.format("--daemon-address=%s", daemon), String.format("--wallet-file=%s", walletPath), String.format("--password=%s", walletPassword)};
ProcessBuilder builder = new ProcessBuilder(cmd)/*.redirectInput(Redirect.INHERIT)*/.redirectError(Redirect.INHERIT).redirectOutput(Redirect.INHERIT);
this.process = builder.start();
this.process.waitFor();
} catch (Exception e)
{
log.error("Can't launch DERO Wallet!!");
e.printStackTrace();
return;
}
});
this.thread.start();
log.info("DERO Wallet is now launched!");
}
this.api = new DeroWallet(host, port, username, password);
log.info("Wallet API is now initialized!");
}
public boolean hasEnoughFunds(String id, BigDecimal amount)
{
return getFunds(id).compareTo(amount) >= 0;
}
public void addFunds(String id, BigDecimal amount)
{
db.setBalance(id, getFunds(id).add(amount));
}
public void removeFunds(String id, BigDecimal amount)
{
db.setBalance(id, getFunds(id).subtract(amount));
}
public String getAddress(String id) throws RequestException
{
String address = db.getAddress(id);
if (address != null)
return address;
String paymentId = this.api.paymentId();
address = this.api.generateAddress(paymentId);
db.setPaymentId(id, paymentId);
db.setAddress(id, address);
return address;
}
public BigDecimal getFunds(String id)
{
return db.getBalance(id);
}
public ArangoDatabaseService getDB()
{
return this.db;
}
public void addUnconfirmedFunds(String userId, BigDecimal amount)
{
db.setUnconfirmedBalance(userId, getUnconfirmedFunds(userId).add(amount));
}
public BigDecimal getUnconfirmedFunds(String userId)
{
return db.getUnconfirmedBalance(userId);
}
public void removeUnconfirmedFunds(String userId, BigDecimal amount)
{
db.setUnconfirmedBalance(userId, getUnconfirmedFunds(userId).subtract(amount));
}
public int getSavedWalletHeight()
{
return this.config.at("general.wallet-height", int.class);
}
public void saveWalletHeight(int lastBlockHeight)
{
this.config.set("general.wallet-height", lastBlockHeight);
}
public DeroWallet getApi()
{
return this.api;
}
public Process getProcess()
{
return this.process;
}
}

+ 47
- 0
src/main/java/fr/slixe/tipbot/command/BalanceCommand.java View File

@@ -0,0 +1,47 @@
package fr.slixe.tipbot.command;
import org.krobot.MessageContext;
import org.krobot.command.ArgumentMap;
import org.krobot.command.Command;
import org.krobot.command.CommandHandler;
import com.google.inject.Inject;
import fr.slixe.tipbot.TipBot;
import fr.slixe.tipbot.Wallet;
import net.dv8tion.jda.core.entities.MessageChannel;
import net.dv8tion.jda.core.entities.PrivateChannel;
@Command(value = "balance", desc = "get your balance and deposit address", errorMP = true)
public class BalanceCommand implements CommandHandler {
@Inject
private Wallet wallet;
@Inject
private TipBot bot;
@Override
public Object handle(MessageContext ctx, ArgumentMap args) throws Exception
{
String id = ctx.getUser().getId();
String funds = wallet.getFunds(id).toString();
String unconfirmedFunds = wallet.getUnconfirmedFunds(id).toString();
MessageChannel chan = ctx.getChannel();
if (!(chan instanceof PrivateChannel))
{
chan = ctx.getUser().openPrivateChannel().complete();
}
if (funds.equals("0") || funds.equals("0E-12")) //dirty i know :/
funds = "0.000000000000";
if (unconfirmedFunds.equals("0") || unconfirmedFunds.equals("0E-12"))
unconfirmedFunds = "0.000000000000";
chan.sendMessage(bot.dialog("Balance", String.format(bot.getMessage("balance"), funds, unconfirmedFunds, wallet.getAddress(id)))).queue();
return null;
}
}

+ 9
- 0
src/main/java/fr/slixe/tipbot/command/CommandException.java View File

@@ -0,0 +1,9 @@
package fr.slixe.tipbot.command;

public class CommandException extends Exception {

public CommandException(String message)
{
super(message);
}
}

+ 151
- 0
src/main/java/fr/slixe/tipbot/command/HelpCommand.java View File

@@ -0,0 +1,151 @@
/*
* Copyright 2017 The Krobot Contributors
*
* This file is part of Krobot.
*
* Krobot is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Krobot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Krobot. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.slixe.tipbot.command;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.krobot.MessageContext;
import org.krobot.command.ArgumentMap;
import org.krobot.command.Command;
import org.krobot.command.CommandCall;
import org.krobot.command.CommandHandler;
import org.krobot.command.KrobotCommand;
import org.krobot.runtime.KrobotRuntime;
import org.krobot.util.Dialog;
import org.krobot.util.Markdown;
import org.krobot.util.MessageUtils;
import net.dv8tion.jda.core.entities.MessageChannel;
import net.dv8tion.jda.core.entities.PrivateChannel;
/**
* The Help Command<br><br>
*
*
* An automatic help command.<br>
* It uses an info Dialog and displays all the commands with
* their syntax, their descriptions, and their subcommands
* recursively.
*
* @author Litarvan
* @version 2.2.0
* @since 2.0.0
*/
@Command(value = "help", desc = "Displays the list of commands with their descriptions", errorMP = true)
public class HelpCommand implements CommandHandler
{
private KrobotRuntime runtime;
@Inject
public HelpCommand(KrobotRuntime runtime)
{
this.runtime = runtime;
}
@Override
public Object handle(MessageContext context, ArgumentMap args)
{
MessageChannel chan = context.getChannel();
if (!(chan instanceof PrivateChannel))
{
chan = context.getUser().openPrivateChannel().complete();
}
StringBuilder curMessage = new StringBuilder();
List<StringBuilder> messages = new ArrayList<>();
messages.add(curMessage);
String prefix = runtime.getFilterRunner().getPrefix(context);
if (prefix == null)
{
prefix = "";
}
for (KrobotCommand command : runtime.getCommandManager().getCommands())
{
CommandCall call = new CommandCall(command);
command.getFilters().forEach(f -> f.filter(call, context, null));
if (call.isCancelled())
{
continue;
}
String cmdStr = prefix + toString("", command);
if(curMessage.length() + cmdStr.length() + 4 > MessageUtils.MAX_MESSAGE_CHARS)
{
curMessage = new StringBuilder();
messages.add(curMessage);
}
curMessage.append(cmdStr).append("\n\n");
}
chan.sendMessage(Dialog.info(Markdown.underline("List of commands :"), messages.get(0).toString())).queue();
for(int i = 1; i < messages.size(); i++)
{
chan.sendMessage(Dialog.info(null, messages.get(i).toString())).queue();
}
return null;
}
private String toString(String prefix, KrobotCommand command)
{
StringBuilder string = new StringBuilder();
String str = command.toString("", false);
String label = command.getLabel();
string.append(prefix.replace("└", "├")).append(Markdown.bold(label)).append(str.substring(label.length())).append("\n");
if (command.getDescription() != null && !command.getDescription().trim().isEmpty())
{
if (!prefix.isEmpty())
{
string.append(prefix.substring(0, prefix.indexOf(Markdown.BOLD_MODIFIER)));
}
string.append("> ").append(Markdown.italic(command.getDescription()));
}
if (command.getSubCommands() != null && command.getSubCommands().size() != 0)
{
for (int i = 0; i < command.getSubCommands().size(); i++)
{
if (!(command.getDescription() == null && i == 0))
{
string.append("\n");
}
string.append(toString((i == command.getSubCommands().size() - 1 ? "└" : "├") + "── " + prefix + Markdown.bold(label) + " ", command.getSubCommands().get(i)));
}
}
return string.toString();
}
}

+ 82
- 0
src/main/java/fr/slixe/tipbot/command/InfoCommand.java View File

@@ -0,0 +1,82 @@
package fr.slixe.tipbot.command;
import javax.inject.Inject;
import org.json.JSONObject;
import org.krobot.MessageContext;
import org.krobot.command.ArgumentMap;
import org.krobot.command.Command;
import org.krobot.command.CommandHandler;
import fr.slixe.dero4j.RequestException;
import fr.slixe.tipbot.TipBot;
import fr.slixe.tipbot.Wallet;
import net.dv8tion.jda.core.entities.MessageChannel;
import net.dv8tion.jda.core.entities.PrivateChannel;
@Command(value = "info", desc = "DERO Network information", errorMP = true)
public class InfoCommand implements CommandHandler
{
@Inject
private Wallet wallet;
@Inject
private TipBot bot;
@Override
public Object handle(MessageContext ctx, ArgumentMap args) throws Exception
{
MessageChannel chan = ctx.getChannel();
if (!(chan instanceof PrivateChannel))
{
chan = ctx.getUser().openPrivateChannel().complete();
}
int walletHeight = 0;
try {
walletHeight = this.wallet.getApi().getHeight();
} catch (RequestException e) {
e.printStackTrace();
throw new CommandException("Wallet isn't available!");
}
chan.sendMessage(bot.dialog("Wallet information", "Height: " + walletHeight)).queue();
int height; //stable_height or height?
int topoHeight; //topoheight
double blockTime;
int difficulty;
int txMempool; //tx_pool_size
int totalSupply;
String daemonVersion;
try {
JSONObject json = bot.getDaemon().getInfo();
height = json.getInt("height");
topoHeight = json.getInt("topoheight");
blockTime = json.getDouble("averageblocktime50");
difficulty = json.getInt("difficulty");
txMempool = json.getInt("tx_pool_size");
totalSupply = json.getInt("total_supply");
daemonVersion = json.getString("version");
} catch (RequestException e)
{
throw new CommandException("Daemon isn't available!");
}
StringBuilder builder = new StringBuilder();
builder.append("Height / Topoheight: ").append(height + " / " + topoHeight).append("\n");
builder.append("Average Block Time: ").append(blockTime).append("\n");
builder.append("Difficulty: ").append(difficulty).append("\n");
builder.append("Mempool: ").append(txMempool).append("\n");
builder.append("Total Supply: ").append(totalSupply).append("\n");
builder.append("Daemon Version: ").append(daemonVersion).append("\n");
chan.sendMessage(bot.dialog("Network information", builder.toString())).queue();
return null;
}
}

+ 71
- 0
src/main/java/fr/slixe/tipbot/command/TipCommand.java View File

@@ -0,0 +1,71 @@
package fr.slixe.tipbot.command;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import javax.inject.Inject;
import org.krobot.MessageContext;
import org.krobot.command.ArgumentMap;
import org.krobot.command.Command;
import org.krobot.command.CommandHandler;
import fr.slixe.tipbot.TipBot;
import fr.slixe.tipbot.Wallet;
import net.dv8tion.jda.core.entities.User;
@Command(value = "tip <to:user> <amount>", desc = "Send some DERO to your friends", errorMP = true)
public class TipCommand implements CommandHandler {
private static final DecimalFormat format = new DecimalFormat("#.############");
@Inject
private Wallet wallet;
@Inject
private TipBot bot;
@Override
public Object handle(MessageContext ctx, ArgumentMap args) throws Exception
{
User to = args.get("to");
if (to.isBot())
throw new CommandException(bot.getMessage("tip.err.tip-bot"));
if (to.getId().equals(ctx.getUser().getId()))
throw new CommandException(bot.getMessage("tip.err.tip-yourself"));
BigDecimal amount;
try {
amount = new BigDecimal(args.get("amount", String.class)).setScale(12, RoundingMode.DOWN);
if (amount.signum() != 1)
throw new CommandException(bot.getMessage("tip.err.positive-value"));
if (amount.scale() > 12)
throw new CommandException(bot.getMessage("tip.err.scale"));
}
catch (NumberFormatException e)
{
throw new CommandException(String.format(bot.getMessage("tip.err.invalid-amount"), ctx.getUser().getAsMention()));
}
String id = ctx.getUser().getId();
if(!wallet.hasEnoughFunds(id, amount))
throw new CommandException(bot.getMessage("tip.err.not-enough"));
wallet.removeFunds(id, amount);
wallet.addFunds(to.getId(), amount);
String strAmount = format.format(amount);
to.openPrivateChannel().queue((e) -> {
e.sendMessage(bot.dialog("Tip incoming!", String.format(bot.getMessage("tip.incoming"), strAmount, ctx.getUser().getAsTag()))).queue();
});
ctx.getUser().openPrivateChannel().queue((e) -> {
e.sendMessage(bot.dialog("Tip Bot", String.format(bot.getMessage("tip.sent"), strAmount, to.getAsMention()))).queue();
});
return bot.dialog("Tip Bot", String.format(bot.getMessage("tip.general"), ctx.getUser().getAsMention(), strAmount, to.getAsMention()));
}
}

+ 79
- 0
src/main/java/fr/slixe/tipbot/command/WithdrawCommand.java View File

@@ -0,0 +1,79 @@
package fr.slixe.tipbot.command;
import java.math.BigDecimal;
import org.krobot.MessageContext;
import org.krobot.command.ArgumentMap;
import org.krobot.command.Command;
import org.krobot.command.CommandHandler;
import org.krobot.util.Dialog;
import com.google.inject.Inject;
import fr.slixe.dero4j.RequestException;
import fr.slixe.tipbot.TipBot;
import fr.slixe.tipbot.Wallet;
import net.dv8tion.jda.core.entities.MessageChannel;
import net.dv8tion.jda.core.entities.PrivateChannel;
@Command(value = "withdraw <amount> <address>", desc = "withdraw your coins from bot", errorMP = true)
public class WithdrawCommand implements CommandHandler {
@Inject
private Wallet wallet;
@Inject
private TipBot bot;
@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)
throw new CommandException(bot.getMessage("withdraw.err.positive-value"));
}
catch (NumberFormatException e)
{
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);
} catch (RequestException e)
{
e.printStackTrace();
throw new CommandException(e.getMessage());
}
String tx;
try {
tx = wallet.getApi().transfer(address, amount);
} catch (RequestException e) {
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;
}
}

+ 69
- 0
src/main/java/fr/slixe/tipbot/task/VerifyTask.java View File

@@ -0,0 +1,69 @@
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 com.arangodb.entity.BaseDocument;
import com.google.inject.Inject;
import fr.slixe.dero4j.RequestException;
import fr.slixe.tipbot.Wallet;
public class VerifyTask extends TimerTask {
private static final Logger log = LoggerFactory.getLogger("VerifyTask");
@Inject
private Wallet wallet;
public VerifyTask() {}
@Override
public void run()
{
int blockHeight;
try {
blockHeight = wallet.getApi().getHeight();
} catch (RequestException e) {
log.error("Looks like wallet isn't reachable...");
return;
}
List<BaseDocument> docs = wallet.getDB().getUnconfirmedTxs();
for (BaseDocument doc : docs)
{
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;
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))
{
this.wallet.getDB().removeTx(txHash);
continue;
}
else {
this.wallet.addFunds(userId, amount);
this.wallet.removeUnconfirmedFunds(userId, amount);
}
}
} catch (RequestException e) {
log.error(e.getMessage());
continue;
}
this.wallet.getDB().updateTx(txHash, diff);
}
}
}

+ 85
- 0
src/main/java/fr/slixe/tipbot/task/WalletTask.java View File

@@ -0,0 +1,85 @@
package fr.slixe.tipbot.task;
import java.util.List;
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.User;
import fr.slixe.tipbot.Wallet;
public class WalletTask extends TimerTask
{
private static final Logger log = LoggerContext.getContext().getLogger("WalletTask");
@Inject
private Wallet wallet;
private int lastBlockHeight = 0;
public WalletTask() {}
@Override
public void run()
{
this.lastBlockHeight = this.wallet.getSavedWalletHeight();
int blockHeight;
int diff = 0;
try {
blockHeight = this.wallet.getApi().getHeight();