commit 52c2c9db5e13ae80f8a9f3a9453b0a00cbc3505b Author: Nelson R. Perez Date: Mon Nov 21 12:50:30 2016 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c29a02a --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Mac OS X file +.DS_Store + +# Gradle +# ------ +.gradle +/build +/buildSrc/build +/subprojects/*/build +/subprojects/docs/src/samples/*/*/build +/subprojects/internal-android-performance-testing/build-android-libs + +# IDEA +# ---- +.idea +.shelf +/*.iml +/*.ipr +/*.iws +/buildSrc/*.iml +/buildSrc/*.ipr +/buildSrc/*.iws +/buildSrc/out +/out +/subprojects/*/*.iml +/subprojects/*/out + +# Eclipse +# ------- +*.classpath +*.project +*.settings +/bin +/subprojects/*/bin +atlassian-ide-plugin.xml + +# NetBeans +# -------- +.nb-gradle +.nb-gradle-properties + +# Vim +# --- +*.sw[op] + +# Emacs +# ----- +*~ + +# Textmate +# -------- +.textmate + +# Sublime Text +# ------------ +*.sublime-* + +# jEnv +# ---- +.java-version + +# OS X +# ---- +.DS_Store + +# HPROF +# ----- +*.hprof + +# Work dirs +# --------- +/incoming-distributions +/intTestHomeDir + +# Logs +# ---- +/*.log diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2f89633 --- /dev/null +++ b/build.gradle @@ -0,0 +1,15 @@ +group 'com.luminiasoft' +version '0.1-SNAPSHOT' + +apply plugin: 'java' + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' + compile 'com.neovisionaries:nv-websocket-client:1.30' + compile 'org.bitcoinj:bitcoinj-core:0.14.3' + compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ca78035 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..54748ef --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Nov 21 11:33:11 PET 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## 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="" + +# 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f6d5974 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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= + +@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 +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0c35b70 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'fullerene' +include 'application' + diff --git a/src/main/java/com/luminiasoft/bitshares/Asset.java b/src/main/java/com/luminiasoft/bitshares/Asset.java new file mode 100644 index 0000000..e345ec9 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Asset.java @@ -0,0 +1,13 @@ +package com.luminiasoft.bitshares; + +import com.luminiasoft.bitshares.interfaces.JsonSerializable; + +/** + * Created by nelson on 11/9/16. + */ +public class Asset extends GrapheneObject { + + public Asset(String id) { + super(id); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/AssetAmount.java b/src/main/java/com/luminiasoft/bitshares/AssetAmount.java new file mode 100644 index 0000000..344c957 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/AssetAmount.java @@ -0,0 +1,88 @@ +package com.luminiasoft.bitshares; + +import com.google.common.primitives.UnsignedLong; +import com.google.gson.*; +import com.luminiasoft.bitshares.interfaces.ByteSerializable; +import com.luminiasoft.bitshares.interfaces.JsonSerializable; + +import java.lang.reflect.Type; + +/** + * Created by nelson on 11/7/16. + */ +public class AssetAmount implements ByteSerializable, JsonSerializable{ + /** + * Constants used in the JSON serialization procedure. + */ + public static final String KEY_AMOUNT = "amount"; + public static final String KEY_ASSET_ID = "asset_id"; + + private UnsignedLong amount; + private Asset asset; + + public AssetAmount(UnsignedLong amount, Asset asset){ + this.amount = amount; + this.asset = asset; + } + + public void setAmount(UnsignedLong amount){ + this.amount = amount; + } + + public UnsignedLong getAmount(){ + return this.amount; + } + + public Asset getAsset(){ return this.asset; } + @Override + public byte[] toBytes() { + byte[] serialized = new byte[8 + 1]; + byte[] amountBytes = this.amount.bigIntegerValue().toByteArray(); + serialized[serialized.length - 1] = (byte) asset.instance; + + for(int i = 0; i < amountBytes.length; i++) + serialized[i] = amountBytes[amountBytes.length - 1 - i]; + + return serialized; + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetSerializer()); + return gsonBuilder.create().toJson(this); + } + + @Override + public JsonObject toJsonObject() { + JsonObject jsonAmount = new JsonObject(); + jsonAmount.addProperty(KEY_AMOUNT, amount); + jsonAmount.addProperty(KEY_ASSET_ID, asset.getObjectId()); + return jsonAmount; + } + + /** + * Custom serializer used to translate this object into the JSON-formatted entry we need for a transaction. + */ + public static class AssetSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(AssetAmount assetAmount, Type type, JsonSerializationContext jsonSerializationContext) { + JsonObject obj = new JsonObject(); + obj.addProperty(KEY_AMOUNT, assetAmount.amount); + obj.addProperty(KEY_ASSET_ID, assetAmount.asset.getObjectId()); + return obj; + } + } + + public static class AssetDeserializer implements JsonDeserializer { + + @Override + public AssetAmount deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + Long amount = json.getAsJsonObject().get(KEY_AMOUNT).getAsLong(); + String assetId = json.getAsJsonObject().get(KEY_ASSET_ID).getAsString(); + AssetAmount assetAmount = new AssetAmount(UnsignedLong.valueOf(amount), new Asset(assetId)); + return assetAmount; + } + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/BaseOperation.java b/src/main/java/com/luminiasoft/bitshares/BaseOperation.java new file mode 100644 index 0000000..eaef155 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/BaseOperation.java @@ -0,0 +1,20 @@ +package com.luminiasoft.bitshares; + +import com.luminiasoft.bitshares.interfaces.ByteSerializable; +import com.luminiasoft.bitshares.interfaces.JsonSerializable; + +/** + * Created by nelson on 11/5/16. + */ +public abstract class BaseOperation implements ByteSerializable, JsonSerializable{ + + protected OperationType type; + + public BaseOperation(OperationType type){ + this.type = type; + } + + public abstract byte getId(); + + public abstract byte[] toBytes(); +} diff --git a/src/main/java/com/luminiasoft/bitshares/BlockData.java b/src/main/java/com/luminiasoft/bitshares/BlockData.java new file mode 100644 index 0000000..6a2a340 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/BlockData.java @@ -0,0 +1,101 @@ +package com.luminiasoft.bitshares; + +import com.luminiasoft.bitshares.interfaces.ByteSerializable; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * This class encapsulates all block-related information needed in order to build a valid transaction. + */ +public class BlockData implements ByteSerializable { + private final int REF_BLOCK_NUM_BYTES = 2; + private final int REF_BLOCK_PREFIX_BYTES = 4; + private final int REF_BLOCK_EXPIRATION_BYTES = 4; + + private int refBlockNum; + private long refBlockPrefix; + private long relativeExpiration; + + /** + * Block data constructor + * @param ref_block_num: Least significant 16 bits from the reference block number. + * If "relative_expiration" is zero, this field must be zero as well. + * @param ref_block_prefix: The first non-block-number 32-bits of the reference block ID. + * Recall that block IDs have 32 bits of block number followed by the + * actual block hash, so this field should be set using the second 32 bits + * in the block_id_type + * @param relative_expiration: This field specifies the number of block intervals after the + * reference block until this transaction becomes invalid. If this field is + * set to zero, the "ref_block_prefix" is interpreted as an absolute timestamp + * of the time the transaction becomes invalid. + */ + public BlockData(int ref_block_num, long ref_block_prefix, long relative_expiration){ + this.refBlockNum = ref_block_num; + this.refBlockPrefix = ref_block_prefix; + this.relativeExpiration = relative_expiration; + } + + /** + * Block data constructor that takes in raw blockchain information. + * @param head_block_number: The last block number. + * @param head_block_id: The last block apiId. + * @param relative_expiration: The relative expiration + */ + public BlockData(long head_block_number, String head_block_id, long relative_expiration){ + String hashData = head_block_id.substring(8, 16); + StringBuilder builder = new StringBuilder(); + for(int i = 0; i < 8; i = i + 2){ + builder.append(hashData.substring(6 - i, 8 - i)); + } + this.refBlockNum = ((int) head_block_number ) & 0xFFFF; + this.refBlockPrefix = Long.parseLong(builder.toString(), 16); + this.relativeExpiration = relative_expiration; + } + + public int getRefBlockNum() { + return refBlockNum; + } + + public void setRefBlockNum(int refBlockNum) { + this.refBlockNum = refBlockNum; + } + + public long getRefBlockPrefix() { + return refBlockPrefix; + } + + public void setRefBlockPrefix(long refBlockPrefix) { + this.refBlockPrefix = refBlockPrefix; + } + + public long getRelativeExpiration() { + return relativeExpiration; + } + + public void setRelativeExpiration(long relativeExpiration) { + this.relativeExpiration = relativeExpiration; + } + + + @Override + public byte[] toBytes() { + // Allocating a fixed length byte array, since we will always need + // 2 bytes for the ref_block_num value + // 4 bytes for the ref_block_prefix value + // 4 bytes for the relative_expiration + + byte[] result = new byte[REF_BLOCK_NUM_BYTES + REF_BLOCK_PREFIX_BYTES + REF_BLOCK_EXPIRATION_BYTES]; + for(int i = 0; i < result.length; i++){ + if(i < REF_BLOCK_NUM_BYTES){ + result[i] = (byte) (this.refBlockNum >> 8 * i); + }else if(i >= REF_BLOCK_NUM_BYTES && i < REF_BLOCK_NUM_BYTES + REF_BLOCK_PREFIX_BYTES){ + result[i] = (byte) (this.refBlockPrefix >> 8 * (i - REF_BLOCK_NUM_BYTES)); + }else{ + result[i] = (byte) (this.relativeExpiration >> 8 * (i - REF_BLOCK_NUM_BYTES + REF_BLOCK_PREFIX_BYTES)); + } + } + return result; + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/BrainKey.java b/src/main/java/com/luminiasoft/bitshares/BrainKey.java new file mode 100644 index 0000000..9445bba --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/BrainKey.java @@ -0,0 +1,31 @@ +package com.luminiasoft.bitshares; + +import org.bitcoinj.core.ECKey; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by nelson on 11/19/16. + */ +public class BrainKey { + + private ECKey mPrivateKey; + + public BrainKey(String words, int sequence){ + String encoded = String.format("%s %d", words, sequence); + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + byte[] bytes = md.digest(encoded.getBytes("UTF-8")); + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + byte[] result = sha256.digest(bytes); + System.out.println("hash: "+Util.bytesToHex(result)); + //TODO: Transform this final result into a ECKey private key (mPrivateKey) + } catch (NoSuchAlgorithmException e) { + System.out.println("NoSuchAlgotithmException. Msg: "+e.getMessage()); + } catch (UnsupportedEncodingException e) { + System.out.println("UnsupportedEncodingException. Msg: "+e.getMessage()); + } + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/Chains.java b/src/main/java/com/luminiasoft/bitshares/Chains.java new file mode 100644 index 0000000..31cc615 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Chains.java @@ -0,0 +1,16 @@ +package com.luminiasoft.bitshares; + +/** + * Created by nelson on 11/8/16. + */ +public class Chains { + public static class BITSHARES { + public static final String CHAIN_ID = "4018d7844c78f6a6c41c6a552b898022310fc5dec06da467ee7905a8dad512c8"; + } + public static class GRAPHENE { + public static final String CHAIN_ID = "b8d1603965b3eb1acba27e62ff59f74efa3154d43a4188d381088ac7cdf35539"; + } + public static class TEST { + public static final String CHAIN_ID = "39f5e2ede1f8bc1a3a54a7914414e3779e33193f1f5693510e73cb7a87617447"; + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/Extension.java b/src/main/java/com/luminiasoft/bitshares/Extension.java new file mode 100644 index 0000000..f406bea --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Extension.java @@ -0,0 +1,8 @@ +package com.luminiasoft.bitshares; + +/** + * Created by nelson on 11/9/16. + */ +public class Extension { + //TODO: Give this class a proper implementation +} diff --git a/src/main/java/com/luminiasoft/bitshares/GrapheneObject.java b/src/main/java/com/luminiasoft/bitshares/GrapheneObject.java new file mode 100644 index 0000000..ede0e47 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/GrapheneObject.java @@ -0,0 +1,33 @@ +package com.luminiasoft.bitshares; + + + +/** + *

+ * Generic class used to represent a graphene object as defined in + * + *

+ * Created by nelson on 11/8/16. + */ +public class GrapheneObject { + protected int space; + protected int type; + protected long instance; + + public GrapheneObject(String id){ + String[] parts = id.split("\\."); + if(parts.length == 3){ + this.space = Integer.parseInt(parts[0]); + this.type = Integer.parseInt(parts[1]); + this.instance = Long.parseLong(parts[2]); + } + } + + /** + * + * @return: A String containing the full object apiId in the form {space}.{type}.{instance} + */ + public String getObjectId(){ + return String.format("%d.%d.%d", space, type, instance); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/Main.java b/src/main/java/com/luminiasoft/bitshares/Main.java new file mode 100644 index 0000000..1ee53dc --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Main.java @@ -0,0 +1,64 @@ +package com.luminiasoft.bitshares; + +import org.bitcoinj.core.ECKey; + +import java.io.IOException; + +public class Main { + // Brain key from Nelson's app referencing the bilthon-83 account + public static final String BRAIN_KEY = "PUMPER ISOTOME SERE STAINER CLINGER MOONLIT CHAETA UPBRIM AEDILIC BERTHER NIT SHAP SAID SHADING JUNCOUS CHOUGH"; + + // WIF from Nelson's app referencing the bilthon-83 account + public static final String WIF = "5J96pne45qWM1WpektoeazN6k9Mt93jQ7LyueRxFfEMTiy6yxjM"; + + // WIF from the cli_wallet instance +// public static final String WIF = "5KMzB2GqGhnh7ufhgddmz1eKPHS72uTLeL9hHjSvPb1UywWknF5"; + public static final String EXTERNAL_SIGNATURE = "1f36c41acb774fcbc9c231b5895ec9701d6872729098d8ea56d78dda72a6b54252694db85d7591de5751b7aea06871da15d63a1028758421607ffc143e53ef3306"; + + // Static block information used for transaction serialization tests + public static int REF_BLOCK_NUM = 56204; + public static int REF_BLOCK_PREFIX = 1614747814; + public static int RELATIVE_EXPIRATION = 1478385607; + + public static void main(String[] args) { + Test test = new Test(); +// test.testTransactionSerialization(); +// ECKey.ECDSASignature signature = test.testSigning(); + +// try { +// test.testWebSocketTransfer(); +// } catch (IOException e) { +// e.printStackTrace(); +// } + +// test.testCustomSerializer(); + +// test.testTransactionSerialization(); + +// test.testLoginSerialization(); + +// test.testNetworkBroadcastSerialization(); + +// test.testNetworkBroadcastDeserialization(); + +// test.testGetDynamicParams(); + +// test.testGetRequiredFeesSerialization(); + +// test.testRequiredFeesResponse(); + +// test.testTransactionBroadcastSequence(); + +// test.testAccountLookupDeserialization(); + +// test.testPrivateKeyManipulations(); + +// test.testGetAccountByName(); + +// test.testGetRequiredFees(); + +// test.testRandomNumberGeneration(); + + test.testBrainKeyOperations(); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/Memo.java b/src/main/java/com/luminiasoft/bitshares/Memo.java new file mode 100644 index 0000000..67650b3 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Memo.java @@ -0,0 +1,15 @@ +package com.luminiasoft.bitshares; + +import com.luminiasoft.bitshares.interfaces.ByteSerializable; + +/** + * Created by nelson on 11/9/16. + */ +public class Memo implements ByteSerializable { + //TODO: Give this class a proper implementation + + @Override + public byte[] toBytes() { + return new byte[1]; + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/OperationType.java b/src/main/java/com/luminiasoft/bitshares/OperationType.java new file mode 100644 index 0000000..ae26a48 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/OperationType.java @@ -0,0 +1,52 @@ +package com.luminiasoft.bitshares; + +/** + * Created by nelson on 11/6/16. + */ +public enum OperationType { + transfer_operation, + limit_order_create_operation, + limit_order_cancel_operation, + call_order_update_operation, + fill_order_operation, // VIRTUAL + account_create_operation, + account_update_operation, + account_whitelist_operation, + account_upgrade_operation, + account_transfer_operation, + asset_create_operation, + asset_update_operation, + asset_update_bitasset_operation, + asset_update_feed_producers_operation, + asset_issue_operation, + asset_reserve_operation, + asset_fund_fee_pool_operation, + asset_settle_operation, + asset_global_settle_operation, + asset_publish_feed_operation, + witness_create_operation, + witness_update_operation, + proposal_create_operation, + proposal_update_operation, + proposal_delete_operation, + withdraw_permission_create_operation, + withdraw_permission_update_operation, + withdraw_permission_claim_operation, + withdraw_permission_delete_operation, + committee_member_create_operation, + committee_member_update_operation, + committee_member_update_global_parameters_operation, + vesting_balance_create_operation, + vesting_balance_withdraw_operation, + worker_create_operation, + custom_operation, + assert_operation, + balance_claim_operation, + override_transfer_operation, + transfer_to_blind_operation, + blind_transfer_operation, + transfer_from_blind_operation, + asset_settle_cancel_operation, // VIRTUAL + asset_claim_fees_operation, + fba_distribute_operation // VIRTUAL +} diff --git a/src/main/java/com/luminiasoft/bitshares/RPC.java b/src/main/java/com/luminiasoft/bitshares/RPC.java new file mode 100644 index 0000000..389d5a1 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/RPC.java @@ -0,0 +1,13 @@ +package com.luminiasoft.bitshares; + +/** + * Created by nelson on 11/16/16. + */ +public class RPC { + public static final String CALL_LOGIN = "login"; + public static final String CALL_NETWORK_BROADCAST = "network_broadcast"; + public static final String CALL_GET_ACCOUNT_BY_NAME = "get_account_by_name"; + public static final String CALL_GET_DYNAMIC_GLOBAL_PROPERTIES = "get_dynamic_global_properties"; + public static final String CALL_BROADCAST_TRANSACTION = "broadcast_transaction"; + public static final String CALL_GET_REQUIRED_FEES = "get_required_fees"; +} diff --git a/src/main/java/com/luminiasoft/bitshares/Test.java b/src/main/java/com/luminiasoft/bitshares/Test.java new file mode 100644 index 0000000..e143343 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Test.java @@ -0,0 +1,509 @@ +package com.luminiasoft.bitshares; + +import com.google.common.primitives.UnsignedLong; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.luminiasoft.bitshares.errors.MalformedTransactionException; +import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; +import com.luminiasoft.bitshares.models.*; +import com.luminiasoft.bitshares.ws.GetAccountByName; +import com.luminiasoft.bitshares.ws.GetRequiredFees; +import com.luminiasoft.bitshares.ws.TransactionBroadcastSequence; +import com.neovisionaries.ws.client.*; +import org.bitcoinj.core.*; +import org.spongycastle.crypto.Digest; +import org.spongycastle.crypto.digests.SHA512Digest; +import org.spongycastle.crypto.prng.DigestRandomGenerator; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Created by nelson on 11/9/16. + */ +public class Test { + public static final String WITNESS_URL = "ws://api.devling.xyz:8088"; + private Transaction transaction; + + public Transaction getTransaction() { + return transaction; + } + + + private WitnessResponseListener mListener = new WitnessResponseListener() { + + @Override + public void onSuccess(WitnessResponse response) { + if(response.result.getClass() == AccountProperties.class){ + AccountProperties accountProperties = (AccountProperties) response.result; + System.out.println("Got account properties"); + System.out.println("id: "+accountProperties.id); + }else if(response.result.getClass() == ArrayList.class){ + List l = (List) response.result; + if(l.size() > 0){ + if(l.get(0).getClass() == AssetAmount.class){ + AssetAmount assetAmount = (AssetAmount) l.get(0); + System.out.println("Got fee"); + System.out.println("amount: "+assetAmount.getAmount()+", asset id: "+assetAmount.getAsset().getObjectId()); + } + }else{ + System.out.println("Got empty list!"); + } + }else{ + System.out.println("Got other: "+response.result.getClass()); + } + } + + @Override + public void onError(BaseResponse.Error error) { + System.out.println("onError. message: "+error.message); + } + }; + + public ECKey.ECDSASignature testSigning() { + byte[] serializedTransaction = this.transaction.toBytes(); + Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); + byte[] bytesDigest = hash.getBytes(); + ECKey sk = transaction.getPrivateKey(); + ECKey.ECDSASignature signature = sk.sign(hash); + return signature; + } + + public String testSigningMessage() { + byte[] serializedTransaction = this.transaction.toBytes(); + Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); + ECKey sk = transaction.getPrivateKey(); + return sk.signMessage(hash.toString()); + } + + public byte[] signMessage() { + byte[] serializedTransaction = this.transaction.toBytes(); + Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); + System.out.println(">> digest <<"); + System.out.println(Util.bytesToHex(hash.getBytes())); + ECKey sk = transaction.getPrivateKey(); + System.out.println("Private key bytes"); + System.out.println(Util.bytesToHex(sk.getPrivKeyBytes())); + boolean isCanonical = false; + int recId = -1; + ECKey.ECDSASignature sig = null; + while (!isCanonical) { + sig = sk.sign(hash); + if (!sig.isCanonical()) { + System.out.println("Signature was not canonical, retrying"); + continue; + } else { + System.out.println("Signature is canonical"); + isCanonical = true; + } + // Now we have to work backwards to figure out the recId needed to recover the signature. + for (int i = 0; i < 4; i++) { + ECKey k = ECKey.recoverFromSignature(i, sig, hash, sk.isCompressed()); + if (k != null && k.getPubKeyPoint().equals(sk.getPubKeyPoint())) { + recId = i; + break; + } else { + if (k == null) { + System.out.println("Recovered key was null"); + } + if (k.getPubKeyPoint().equals(sk.getPubKeyPoint())) { + System.out.println("Recovered pub point is not equal to sk pub point"); + } + } + } + if (recId == -1) + throw new RuntimeException("Could not construct a recoverable key. This should never happen."); + } + int headerByte = recId + 27 + (sk.isCompressed() ? 4 : 0); + byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S + sigData[0] = (byte) headerByte; + System.arraycopy(Utils.bigIntegerToBytes(sig.r, 32), 0, sigData, 1, 32); + System.arraycopy(Utils.bigIntegerToBytes(sig.s, 32), 0, sigData, 33, 32); + System.out.println("recId: " + recId); + System.out.println("r: " + Util.bytesToHex(sig.r.toByteArray())); + System.out.println("s: " + Util.bytesToHex(sig.s.toByteArray())); + return sigData; +// return new String(Base64.encode(sigData), Charset.forName("UTF-8")); + } + + public byte[] testTransactionSerialization(long head_block_number, String head_block_id, long relative_expiration) { + BlockData blockData = new BlockData(head_block_number, head_block_id, relative_expiration); + + ArrayList operations = new ArrayList(); + UserAccount from = new UserAccount("1.2.138632"); + UserAccount to = new UserAccount("1.2.129848"); + AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120")); + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf(264174), new Asset("1.3.0")); + operations.add(new Transfer(from, to, amount, fee)); + this.transaction = new Transaction(Main.WIF, blockData, operations); + byte[] serializedTransaction = this.transaction.toBytes(); + System.out.println("Serialized transaction"); + System.out.println(Util.bytesToHex(serializedTransaction)); + return serializedTransaction; + } + + public void testWebSocketTransfer() throws IOException { + String login = "{\"id\":%d,\"method\":\"call\",\"params\":[1,\"login\",[\"\",\"\"]]}"; + String getDatabaseId = "{\"method\": \"call\", \"params\": [1, \"database\", []], \"jsonrpc\": \"2.0\", \"id\": %d}"; + String getHistoryId = "{\"method\": \"call\", \"params\": [1, \"history\", []], \"jsonrpc\": \"2.0\", \"id\": %d}"; + String getNetworkBroadcastId = "{\"method\": \"call\", \"params\": [1, \"network_broadcast\", []], \"jsonrpc\": \"2.0\", \"id\": %d}"; + String getDynamicParameters = "{\"method\": \"call\", \"params\": [0, \"get_dynamic_global_properties\", []], \"jsonrpc\": \"2.0\", \"id\": %d}"; + String rawPayload = "{\"method\": \"call\", \"params\": [%d, \"broadcast_transaction\", [{\"expiration\": \"%s\", \"signatures\": [\"%s\"], \"operations\": [[0, {\"fee\": {\"amount\": 264174, \"asset_id\": \"1.3.0\"}, \"amount\": {\"amount\": 100, \"asset_id\": \"1.3.120\"}, \"to\": \"1.2.129848\", \"extensions\": [], \"from\": \"1.2.138632\"}]], \"ref_block_num\": %d, \"extensions\": [], \"ref_block_prefix\": %d}]], \"jsonrpc\": \"2.0\", \"id\": %d}"; + +// String url = "wss://bitshares.openledger.info/ws"; + String url = "ws://api.devling.xyz:8088"; + WebSocketFactory factory = new WebSocketFactory().setConnectionTimeout(5000); + + // Create a WebSocket. The timeout value set above is used. + WebSocket ws = factory.createSocket(url); + + ws.addListener(new WebSocketAdapter() { + + private DynamicGlobalProperties dynProperties; + private int networkBroadcastApiId; + + @Override + public void onConnected(WebSocket websocket, Map> headers) throws Exception { + System.out.println("onConnected"); + String payload = String.format(login, 1); + System.out.println(">>"); + System.out.println(payload); + websocket.sendText(payload); + } + + @Override + public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) throws Exception { + System.out.println("onDisconnected"); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + System.out.println("<<"); + String response = frame.getPayloadText(); + System.out.println(response); + Gson gson = new Gson(); + BaseResponse baseResponse = gson.fromJson(response, BaseResponse.class); +// if(baseResponse.id.equals("1")){ +// String payload = String.format(getDatabaseId, 2); +// System.out.println(">>"); +// System.out.println(payload); +// websocket.sendText(payload); +// }else if(baseResponse.id.equals("2")){ +// String payload = String.format(getHistoryId, 3); +// System.out.println(">>"); +// System.out.println(payload); +// websocket.sendText(payload); +// }else if(baseResponse.id.equals("3")){ + if (baseResponse.id == 1) { + String payload = String.format(getNetworkBroadcastId, 2); + System.out.println(">>"); + System.out.println(payload); + websocket.sendText(payload); +// }else if(baseResponse.id.equals("4")){ + } + if (baseResponse.id == 2) { + String payload = String.format(getDynamicParameters, 3); + Type ApiIdResponse = new TypeToken>() {}.getType(); + WitnessResponse witnessResponse = gson.fromJson(response, ApiIdResponse); + networkBroadcastApiId = witnessResponse.result.intValue(); + System.out.println(">>"); + System.out.println(payload); + websocket.sendText(payload); + } else if (baseResponse.id == 3) { + // Got dynamic properties + Type DynamicGlobalPropertiesResponse = new TypeToken>() { + }.getType(); + WitnessResponse witnessResponse = gson.fromJson(response, DynamicGlobalPropertiesResponse); + dynProperties = witnessResponse.result; + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date date = dateFormat.parse(dynProperties.time); + long expirationTime = (date.getTime() / 1000) + 30; + testTransactionSerialization(dynProperties.head_block_number, dynProperties.head_block_id, expirationTime); + + BlockData blockData = new BlockData(dynProperties.head_block_number, dynProperties.head_block_id, expirationTime); + byte[] signatureBytes = signMessage(); + + String payload = String.format( + rawPayload, + networkBroadcastApiId, + dateFormat.format(new Date(expirationTime * 1000)), + Util.bytesToHex(signatureBytes), + blockData.getRefBlockNum(), + blockData.getRefBlockPrefix(), + 4); + System.out.println(">>"); + System.out.println(payload); + websocket.sendText(payload); + } + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + System.out.println("onError"); + } + + @Override + public void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws Exception { + System.out.println("onUnexpectedError"); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + System.out.println("handleCallbackError. Msg: " + cause.getMessage()); + StackTraceElement[] stackTrace = cause.getStackTrace(); + for (StackTraceElement line : stackTrace) { + System.out.println(line.toString()); + } + } + }); + try { + // Connect to the server and perform an opening handshake. + // This method blocks until the opening handshake is finished. + ws.connect(); + } catch (OpeningHandshakeException e) { + // A violation against the WebSocket protocol was detected + // during the opening handshake. + System.out.println("OpeningHandshakeException"); + } catch (WebSocketException e) { + // Failed to establish a WebSocket connection. + System.out.println("WebSocketException. Msg: " + e.getMessage()); + } + } + + public void testCustomSerializer() { + AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120")); + String jsonAmount = amount.toJsonString(); + System.out.println("JSON amount"); + System.out.println(jsonAmount); + } + + public void testTransactionSerialization() { + try { + Transaction transaction = new TransferTransactionBuilder() + .setSource(new UserAccount("1.2.138632")) + .setDestination(new UserAccount("1.2.129848")) + .setAmount(new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120"))) + .setFee(new AssetAmount(UnsignedLong.valueOf(264174), new Asset("1.3.0"))) + .setBlockData(new BlockData(43408, 1430521623, 1479231969)) + .setPrivateKey(DumpedPrivateKey.fromBase58(null, Main.WIF).getKey()) + .build(); + + ArrayList transactionList = new ArrayList<>(); + transactionList.add(transaction); + ApiCall call = new ApiCall(4, "call", "broadcast_transaction", transactionList, "2.0", 1); + String jsonCall = call.toJsonString(); + System.out.println("json call"); + System.out.println(jsonCall); + } catch (MalformedTransactionException e) { + System.out.println("MalformedTransactionException. Msg: " + e.getMessage()); + } + } + + public void testLoginSerialization() { + ArrayList loginParams = new ArrayList<>(); +// loginParams.add("nelson"); +// loginParams.add("supersecret"); + loginParams.add(null); + loginParams.add(null); + ApiCall loginCall = new ApiCall(1, "login", loginParams, "2.0", 1); + String jsonLoginCall = loginCall.toJsonString(); + System.out.println("login call"); + System.out.println(jsonLoginCall); + } + + public void testNetworkBroadcastSerialization() { + ArrayList params = new ArrayList<>(); + ApiCall networkParamsCall = new ApiCall(3, "network_broadcast", params, "2.0", 1); + String call = networkParamsCall.toJsonString(); + System.out.println("network broadcast"); + System.out.println(call); + } + + public void testNetworkBroadcastDeserialization(){ + String response = "{\"id\":2,\"result\":2}"; + Gson gson = new Gson(); + Type ApiIdResponse = new TypeToken>() {}.getType(); + WitnessResponse witnessResponse = gson.fromJson(response, ApiIdResponse); + } + + public void testGetDynamicParams() { + ArrayList emptyParams = new ArrayList<>(); + ApiCall getDynamicParametersCall = new ApiCall(0, "get_dynamic_global_properties", emptyParams, "2.0", 0); + System.out.println(getDynamicParametersCall.toJsonString()); + } + + public void testRequiredFeesResponse() { + String response = "{\"id\":1,\"result\":[{\"amount\":264174,\"asset_id\":\"1.3.0\"}]}"; + Type AccountLookupResponse = new TypeToken>>() {}.getType(); + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetDeserializer()); + WitnessResponse> witnessResponse = gsonBuilder.create().fromJson(response, AccountLookupResponse); + for(AssetAmount assetAmount : witnessResponse.result){ + System.out.println("asset : "+assetAmount.toJsonString()); + } + } + + public void testTransactionBroadcastSequence(){ + String url = "ws://api.devling.xyz:8088"; + WitnessResponseListener listener = new WitnessResponseListener() { + @Override + public void onSuccess(WitnessResponse response) { + System.out.println("onSuccess"); + } + + @Override + public void onError(BaseResponse.Error error) { + System.out.println("onError"); + System.out.println(error.data.message); + } + }; + + try{ + Transaction transaction = new TransferTransactionBuilder() + .setSource(new UserAccount("1.2.138632")) + .setDestination(new UserAccount("1.2.129848")) + .setAmount(new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120"))) + .setFee(new AssetAmount(UnsignedLong.valueOf(264174), new Asset("1.3.0"))) + .setBlockData(new BlockData(43408, 1430521623, 1479231969)) + .setPrivateKey(DumpedPrivateKey.fromBase58(null, Main.WIF).getKey()) + .build(); + + ArrayList transactionList = new ArrayList<>(); + transactionList.add(transaction); + ApiCall call = new ApiCall(4, "call", "broadcast_transaction", transactionList, "2.0", 1); + + WebSocketFactory factory = new WebSocketFactory().setConnectionTimeout(5000); + try { + WebSocket mWebSocket = factory.createSocket(url); + mWebSocket.addListener(new TransactionBroadcastSequence(transaction, listener)); + mWebSocket.connect(); + } catch (IOException e) { + System.out.println("IOException. Msg: "+e.getMessage()); + } catch (WebSocketException e) { + System.out.println("WebSocketException. Msg: "+e.getMessage()); + } + }catch(MalformedTransactionException e){ + System.out.println("MalformedTransactionException. Msg: "+e.getMessage()); + } + } + + public void testAccountLookupDeserialization(){ + String response = "{\"id\":1,\"result\":[[\"ken\",\"1.2.3111\"],[\"ken-1\",\"1.2.101491\"],[\"ken-k\",\"1.2.108646\"]]}"; + Type AccountLookupResponse = new TypeToken>>>() {}.getType(); + Gson gson = new Gson(); + WitnessResponse>> witnessResponse = gson.fromJson(response, AccountLookupResponse); + for(int i = 0; i < witnessResponse.result.size(); i++){ + System.out.println("suggested name: "+witnessResponse.result.get(i).get(0)); + } + } + + public void testPrivateKeyManipulations(){ + ECKey privateKey = DumpedPrivateKey.fromBase58(null, Main.WIF).getKey(); + System.out.println("private key..............: "+Util.bytesToHex(privateKey.getSecretBytes())); + System.out.println("public key uncompressed..: "+Util.bytesToHex(privateKey.getPubKey())); + System.out.println("public key compressed....: "+Util.bytesToHex(privateKey.getPubKeyPoint().getEncoded(true))); + System.out.println("base58...................: "+Base58.encode(privateKey.getPubKeyPoint().getEncoded(true))); + System.out.println("base58...................: "+Base58.encode(privateKey.getPubKey())); + String brainKeyWords = "PUMPER ISOTOME SERE STAINER CLINGER MOONLIT CHAETA UPBRIM AEDILIC BERTHER NIT SHAP SAID SHADING JUNCOUS CHOUGH"; + BrainKey brainKey = new BrainKey(brainKeyWords, 0); + } + + public void testGetAccountByName(){ + try { + WebSocketFactory factory = new WebSocketFactory().setConnectionTimeout(5000); + WebSocket mWebSocket = factory.createSocket(WITNESS_URL); + mWebSocket.addListener(new GetAccountByName("bilthon-83", mListener)); + mWebSocket.connect(); + } catch (IOException e) { + System.out.println("IOException. Msg: "+e.getMessage()); + } catch (WebSocketException e) { + System.out.println("WebSocketException. Msg: "+e.getMessage()); + } + } + + public void testGetRequiredFees() { + ArrayList accountParams = new ArrayList<>(); + Asset asset = new Asset("1.3.0"); + UserAccount from = new UserAccount("1.2.138632"); + UserAccount to = new UserAccount("1.2.129848"); + AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120")); + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf(264174), new Asset("1.3.0")); + Transfer transfer = new Transfer(from, to, amount, fee); + ArrayList operations = new ArrayList<>(); + operations.add(transfer); + + accountParams.add(operations); + accountParams.add(asset.getObjectId()); + + try { + WebSocketFactory factory = new WebSocketFactory().setConnectionTimeout(5000); + WebSocket mWebSocket = factory.createSocket(WITNESS_URL); + mWebSocket.addListener(new GetRequiredFees(operations, asset, mListener)); + mWebSocket.connect(); + } catch (IOException e) { + System.out.println("IOException. Msg: "+e.getMessage()); + } catch (WebSocketException e) { + System.out.println("WebSocketException. Msg: "+e.getMessage()); + } + } + + public void testRandomNumberGeneration(){ + byte[] seed = new byte[] { new Long(System.nanoTime()).byteValue() }; + doCountTest(new SHA512Digest(), seed); + } + + private void doCountTest(Digest digest, byte[] seed)//, byte[] expectedXors) + { + DigestRandomGenerator generator = new DigestRandomGenerator(digest); + byte[] output = new byte[digest.getDigestSize()]; + int[] averages = new int[digest.getDigestSize()]; + byte[] ands = new byte[digest.getDigestSize()]; + byte[] xors = new byte[digest.getDigestSize()]; + byte[] ors = new byte[digest.getDigestSize()]; + + generator.addSeedMaterial(seed); + + for (int i = 0; i != 1000000; i++) + { + generator.nextBytes(output); + for (int j = 0; j != output.length; j++) + { + averages[j] += output[j] & 0xff; + ands[j] &= output[j]; + xors[j] ^= output[j]; + ors[j] |= output[j]; + } + } + + for (int i = 0; i != output.length; i++) { + if ((averages[i] / 1000000) != 127) { + System.out.println("average test failed for " + digest.getAlgorithmName()); + } + System.out.println("averages["+i+"] / 1000000: "+averages[i] / 1000000); + if (ands[i] != 0) { + System.out.println("and test failed for " + digest.getAlgorithmName()); + } + if ((ors[i] & 0xff) != 0xff) { + System.out.println("or test failed for " + digest.getAlgorithmName()); + } +// if (xors[i] != expectedXors[i]) { +// System.out.println("xor test failed for " + digest.getAlgorithmName()); +// } + } + } + + /** + * The final purpose of this test is to convert the plain brainkey at Main.BRAIN_KEY + * into the WIF at Main.WIF + */ + public void testBrainKeyOperations(){ + BrainKey brainKey = new BrainKey(Main.BRAIN_KEY, 0); + } + +} diff --git a/src/main/java/com/luminiasoft/bitshares/Transaction.java b/src/main/java/com/luminiasoft/bitshares/Transaction.java new file mode 100644 index 0000000..c1efffa --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Transaction.java @@ -0,0 +1,197 @@ +package com.luminiasoft.bitshares; + +import com.google.common.primitives.Bytes; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.luminiasoft.bitshares.interfaces.ByteSerializable; +import com.luminiasoft.bitshares.interfaces.JsonSerializable; + +import org.bitcoinj.core.DumpedPrivateKey; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Utils; + +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +/** + * Class used to represent a generic graphene transaction. + */ +public class Transaction implements ByteSerializable, JsonSerializable { + private final String TAG = this.getClass().getName(); + + public static final String KEY_EXPIRATION = "expiration"; + public static final String KEY_SIGNATURES = "signatures"; + public static final String KEY_OPERATIONS = "operations"; + public static final String KEY_EXTENSIONS = "extensions"; + public static final String KEY_REF_BLOCK_NUM = "ref_block_num"; + public static final String KEY_REF_BLOCK_PREFIX = "ref_block_prefix"; + + private ECKey privateKey; + private BlockData blockData; + private List operations; + private List extensions; + + /** + * Transaction constructor. + * @param wif: The user's private key in the base58 format. + * @param block_data: Block data containing important information used to sign a transaction. + * @param operation_list: List of operations to include in the transaction. + */ + public Transaction(String wif, BlockData block_data, List operation_list){ + this.privateKey = DumpedPrivateKey.fromBase58(null, wif).getKey(); + this.blockData = block_data; + this.operations = operation_list; + this.extensions = new ArrayList(); + } + + /** + * Transaction constructor. + * @param privateKey : Instance of a ECKey containing the private key that will be used to sign this transaction. + * @param blockData : Block data containing important information used to sign a transaction. + * @param operationList : List of operations to include in the transaction. + */ + public Transaction(ECKey privateKey, BlockData blockData, List operationList){ + this.privateKey = privateKey; + this.blockData = blockData; + this.operations = operationList; + this.extensions = new ArrayList(); + } + + public ECKey getPrivateKey(){ + return this.privateKey; + } + + public List getOperations(){ return this.operations; } + + /** + * Obtains a signature of this transaction. + * @return: A valid signature of the current transaction. + */ + public byte[] getSignature(){ + byte[] serializedTransaction = this.toBytes(); + Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); + boolean isCanonical = false; + int recId = -1; + ECKey.ECDSASignature sig = null; + while(!isCanonical) { + sig = privateKey.sign(hash); + if(!sig.isCanonical()){ + // Signature was not canonical, retrying + continue; + }else{ + // Signature is canonical + isCanonical = true; + } + // Now we have to work backwards to figure out the recId needed to recover the signature. + for (int i = 0; i < 4; i++) { + ECKey k = ECKey.recoverFromSignature(i, sig, hash, privateKey.isCompressed()); + if (k != null && k.getPubKeyPoint().equals(privateKey.getPubKeyPoint())) { + recId = i; + break; + } + } + } + int headerByte = recId + 27 + (privateKey.isCompressed() ? 4 : 0); + byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S + sigData[0] = (byte)headerByte; + System.arraycopy(Utils.bigIntegerToBytes(sig.r, 32), 0, sigData, 1, 32); + System.arraycopy(Utils.bigIntegerToBytes(sig.s, 32), 0, sigData, 33, 32); + return sigData; + } + + /** + * Method that creates a serialized byte array with compact information about this transaction + * that is needed for the creation of a signature. + * @return: byte array with serialized information about this transaction. + */ + public byte[] toBytes(){ + // Creating a List of Bytes and adding the first bytes from the chain apiId + List byteArray = new ArrayList(); + byteArray.addAll(Bytes.asList(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID))); + + // Adding the block data + byteArray.addAll(Bytes.asList(this.blockData.toBytes())); + + // Adding the number of operations + byteArray.add((byte) this.operations.size()); + + // Adding all the operations + for(BaseOperation operation : operations){ + byteArray.add(operation.getId()); + byteArray.addAll(Bytes.asList(operation.toBytes())); + } + + //Adding the number of extensions + byteArray.add((byte) this.extensions.size()); + + for(Extension extension : extensions){ + //TODO: Implement the extension serialization + } + // Adding a last zero byte to match the result obtained by the python-graphenelib code + // I'm not exactly sure what's the meaning of this last zero byte, but for now I'll just + // leave it here and work on signing the transaction. + //TODO: Investigate the origin and meaning of this last byte. + byteArray.add((byte) 0 ); + + return Bytes.toArray(byteArray); + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer()); + return gsonBuilder.create().toJson(this); + } + + @Override + public JsonObject toJsonObject() { + JsonObject obj = new JsonObject(); + + // Formatting expiration time + Date expirationTime = new Date(blockData.getRelativeExpiration() * 1000); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + + // Adding expiration + obj.addProperty(KEY_EXPIRATION, dateFormat.format(expirationTime)); + + // Adding signatures + JsonArray signatureArray = new JsonArray(); + signatureArray.add(Util.bytesToHex(getSignature())); + obj.add(KEY_SIGNATURES, signatureArray); + + JsonArray operationsArray = new JsonArray(); + for(BaseOperation operation : operations){ + operationsArray.add(operation.toJsonObject()); + } + // Adding operations + obj.add(KEY_OPERATIONS, operationsArray); + + // Adding extensions + obj.add(KEY_EXTENSIONS, new JsonArray()); + + // Adding block data + obj.addProperty(KEY_REF_BLOCK_NUM, blockData.getRefBlockNum()); + obj.addProperty(KEY_REF_BLOCK_PREFIX, blockData.getRefBlockPrefix()); + + return obj; + + } + + class TransactionSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(Transaction transaction, Type type, JsonSerializationContext jsonSerializationContext) { + return transaction.toJsonObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java b/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java new file mode 100644 index 0000000..05249a8 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java @@ -0,0 +1,15 @@ +package com.luminiasoft.bitshares; + +import com.luminiasoft.bitshares.errors.MalformedTransactionException; +import org.bitcoinj.core.ECKey; + + +/** + * Created by nelson on 11/14/16. + */ +public abstract class TransactionBuilder { + protected ECKey privateKey; + protected BlockData blockData; + + public abstract Transaction build() throws MalformedTransactionException; +} diff --git a/src/main/java/com/luminiasoft/bitshares/Transfer.java b/src/main/java/com/luminiasoft/bitshares/Transfer.java new file mode 100644 index 0000000..187cbaa --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Transfer.java @@ -0,0 +1,113 @@ +package com.luminiasoft.bitshares; + +import com.google.common.primitives.Bytes; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; + +/** + * Class used to encapsulate the Transfer operation related functionalities. + */ +public class Transfer extends BaseOperation { + public static final String KEY_FEE = "fee"; + public static final String KEY_AMOUNT = "amount"; + public static final String KEY_EXTENSIONS = "extensions"; + public static final String KEY_FROM = "from"; + public static final String KEY_TO = "to"; + + private AssetAmount fee; + private AssetAmount amount; + private UserAccount from; + private UserAccount to; + private Memo memo; + private String[] extensions; + + public Transfer(UserAccount from, UserAccount to, AssetAmount transferAmount, AssetAmount fee){ + super(OperationType.transfer_operation); + this.from = from; + this.to = to; + this.amount = transferAmount; + this.fee = fee; + this.memo = new Memo(); + } + + public Transfer(UserAccount from, UserAccount to, AssetAmount transferAmount){ + super(OperationType.transfer_operation); + this.from = from; + this.to = to; + this.amount = transferAmount; + this.memo = new Memo(); + } + + public void setFee(AssetAmount newFee){ + this.fee = newFee; + } + + @Override + public byte getId() { + return (byte) this.type.ordinal(); + } + + public UserAccount getFrom(){ + return this.from; + } + + public UserAccount getTo(){ + return this.to; + } + + public AssetAmount getAmount(){ + return this.amount; + } + + public AssetAmount getFee(){ + return this.fee; + } + + @Override + public byte[] toBytes() { + byte[] feeBytes = fee.toBytes(); + byte[] fromBytes = from.toBytes(); + byte[] toBytes = to.toBytes(); + byte[] amountBytes = amount.toBytes(); + byte[] memoBytes = memo.toBytes(); + return Bytes.concat(feeBytes, fromBytes, toBytes, amountBytes, memoBytes); + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(Transfer.class, new TransferSerializer()); + return gsonBuilder.create().toJson(this); + } + + @Override + public JsonElement toJsonObject() { + JsonArray array = new JsonArray(); + array.add(this.getId()); + JsonObject jsonObject = new JsonObject(); + jsonObject.add(KEY_FEE, fee.toJsonObject()); + jsonObject.add(KEY_AMOUNT, amount.toJsonObject()); + jsonObject.add(KEY_EXTENSIONS, new JsonArray()); + jsonObject.addProperty(KEY_FROM, from.toJsonString()); + jsonObject.addProperty(KEY_TO, to.toJsonString()); + array.add(jsonObject); + return array; + } + + class TransferSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(Transfer transfer, Type type, JsonSerializationContext jsonSerializationContext) { + JsonArray arrayRep = new JsonArray(); + arrayRep.add(transfer.getId()); + arrayRep.add(toJsonObject()); + return arrayRep; + } + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java b/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java new file mode 100644 index 0000000..61f046c --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java @@ -0,0 +1,86 @@ +package com.luminiasoft.bitshares; + +import com.luminiasoft.bitshares.errors.MalformedTransactionException; +import org.bitcoinj.core.ECKey; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class used to build a transaction containing a transfer operation. + */ +public class TransferTransactionBuilder extends TransactionBuilder { + private List operations; + private UserAccount sourceAccount; + private UserAccount destinationAccount; + private AssetAmount transferAmount; + private AssetAmount feeAmount; + + public TransferTransactionBuilder setPrivateKey(ECKey key){ + this.privateKey = key; + return this; + } + + public TransferTransactionBuilder setBlockData(BlockData blockData){ + this.blockData = blockData; + return this; + } + + public TransferTransactionBuilder setSource(UserAccount source){ + this.sourceAccount = source; + return this; + } + + public TransferTransactionBuilder setDestination(UserAccount destination){ + this.destinationAccount = destination; + return this; + } + + public TransferTransactionBuilder setAmount(AssetAmount amount){ + this.transferAmount = amount; + return this; + } + + public TransferTransactionBuilder setFee(AssetAmount amount){ + this.feeAmount = amount; + return this; + } + + public TransferTransactionBuilder addOperation(Transfer transferOperation){ + if(operations == null){ + operations = new ArrayList(); + } + return this; + } + + @Override + public Transaction build() throws MalformedTransactionException { + if(privateKey == null){ + throw new MalformedTransactionException("Missing private key information"); + }else if(blockData == null){ + throw new MalformedTransactionException("Missing block data information"); + }else if(operations == null){ + // If the operations list has not been set, we might be able to build one with the + // previously provided data. But in order for this to work we have to have all + // source, destination and transfer amount data. + operations = new ArrayList<>(); + if(sourceAccount == null){ + throw new MalformedTransactionException("Missing source account information"); + } + if(destinationAccount == null){ + throw new MalformedTransactionException("Missing destination account information"); + } + if(transferAmount == null){ + throw new MalformedTransactionException("Missing transfer amount information"); + } + Transfer transferOperation; + if(feeAmount == null){ + transferOperation = new Transfer(sourceAccount, destinationAccount, transferAmount); + }else{ + transferOperation = new Transfer(sourceAccount, destinationAccount, transferAmount, feeAmount); + } + operations.add(transferOperation); + } + return new Transaction(privateKey, blockData, operations); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/UserAccount.java b/src/main/java/com/luminiasoft/bitshares/UserAccount.java new file mode 100644 index 0000000..a99eb62 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/UserAccount.java @@ -0,0 +1,48 @@ +package com.luminiasoft.bitshares; + +import com.google.gson.JsonObject; +import com.luminiasoft.bitshares.interfaces.ByteSerializable; +import com.luminiasoft.bitshares.interfaces.JsonSerializable; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Class tha represents a graphene user account. + * Created by nelson on 11/8/16. + */ +public class UserAccount extends GrapheneObject implements ByteSerializable, JsonSerializable { + + /** + * Constructor that expects a user account in the string representation. + * That is in the 1.2.x format. + * @param id: The string representing the account apiId. + */ + public UserAccount(String id) { + super(id); + } + + @Override + public byte[] toBytes() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutput out = new DataOutputStream(byteArrayOutputStream); + try { + Varint.writeUnsignedVarLong(this.instance, out); + } catch (IOException e) { + e.printStackTrace(); + } + return byteArrayOutputStream.toByteArray(); + } + + @Override + public String toJsonString() { + return this.getObjectId(); + } + + @Override + public JsonObject toJsonObject() { + return null; + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/Util.java b/src/main/java/com/luminiasoft/bitshares/Util.java new file mode 100644 index 0000000..635cb16 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Util.java @@ -0,0 +1,28 @@ +package com.luminiasoft.bitshares; + +/** + * Created by nelson on 11/8/16. + */ +public class Util { + final private static char[] hexArray = "0123456789abcdef".toCharArray(); + + public static byte[] hexToBytes(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/Varint.java b/src/main/java/com/luminiasoft/bitshares/Varint.java new file mode 100644 index 0000000..1b37689 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Varint.java @@ -0,0 +1,224 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.luminiasoft.bitshares; + + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + + +/** + *

Encodes signed and unsigned values using a common variable-length + * scheme, found for example in + * + * Google's Protocol Buffers. It uses fewer bytes to encode smaller values, + * but will use slightly more bytes to encode large values.

+ *

+ *

Signed values are further encoded using so-called zig-zag encoding + * in order to make them "compatible" with variable-length encoding.

+ */ +public final class Varint { + + private Varint() { + } + + /** + * Encodes a value using the variable-length encoding from + * + * Google Protocol Buffers. It uses zig-zag encoding to efficiently + * encode signed values. If values are known to be nonnegative, + * {@link #writeUnsignedVarLong(long, DataOutput)} should be used. + * + * @param value value to encode + * @param out to write bytes to + * @throws IOException if {@link DataOutput} throws {@link IOException} + */ + public static void writeSignedVarLong(long value, DataOutput out) throws IOException { + // Great trick from http://code.google.com/apis/protocolbuffers/docs/encoding.html#types + writeUnsignedVarLong((value << 1) ^ (value >> 63), out); + } + + /** + * Encodes a value using the variable-length encoding from + * + * Google Protocol Buffers. Zig-zag is not used, so input must not be negative. + * If values can be negative, use {@link #writeSignedVarLong(long, DataOutput)} + * instead. This method treats negative input as like a large unsigned value. + * + * @param value value to encode + * @param out to write bytes to + * @throws IOException if {@link DataOutput} throws {@link IOException} + */ + public static void writeUnsignedVarLong(long value, DataOutput out) throws IOException { + while ((value & 0xFFFFFFFFFFFFFF80L) != 0L) { + out.writeByte(((int) value & 0x7F) | 0x80); + value >>>= 7; + } + out.writeByte((int) value & 0x7F); + } + + /** + * @see #writeSignedVarLong(long, DataOutput) + */ + public static void writeSignedVarInt(int value, DataOutput out) throws IOException { + // Great trick from http://code.google.com/apis/protocolbuffers/docs/encoding.html#types + writeUnsignedVarInt((value << 1) ^ (value >> 31), out); + } + + /** + * @see #writeUnsignedVarLong(long, DataOutput) + */ + public static void writeUnsignedVarInt(int value, DataOutput out) throws IOException { + while ((value & 0xFFFFFF80) != 0L) { + out.writeByte((value & 0x7F) | 0x80); + value >>>= 7; + } + out.writeByte(value & 0x7F); + } + + public static byte[] writeSignedVarInt(int value) { + // Great trick from http://code.google.com/apis/protocolbuffers/docs/encoding.html#types + return writeUnsignedVarInt((value << 1) ^ (value >> 31)); + } + + /** + * @see #writeUnsignedVarLong(long, DataOutput) + *

+ * This one does not use streams and is much faster. + * Makes a single object each time, and that object is a primitive array. + */ + public static byte[] writeUnsignedVarInt(int value) { + byte[] byteArrayList = new byte[10]; + int i = 0; + while ((value & 0xFFFFFF80) != 0L) { + byteArrayList[i++] = ((byte) ((value & 0x7F) | 0x80)); + value >>>= 7; + } + byteArrayList[i] = ((byte) (value & 0x7F)); + byte[] out = new byte[i + 1]; + for (; i >= 0; i--) { + out[i] = byteArrayList[i]; + } + return out; + } + + /** + * @param in to read bytes from + * @return decode value + * @throws IOException if {@link DataInput} throws {@link IOException} + * @throws IllegalArgumentException if variable-length value does not terminate + * after 9 bytes have been read + * @see #writeSignedVarLong(long, DataOutput) + */ + public static long readSignedVarLong(DataInput in) throws IOException { + long raw = readUnsignedVarLong(in); + // This undoes the trick in writeSignedVarLong() + long temp = (((raw << 63) >> 63) ^ raw) >> 1; + // This extra step lets us deal with the largest signed values by treating + // negative results from read unsigned methods as like unsigned values + // Must re-flip the top bit if the original read value had it set. + return temp ^ (raw & (1L << 63)); + } + + /** + * @param in to read bytes from + * @return decode value + * @throws IOException if {@link DataInput} throws {@link IOException} + * @throws IllegalArgumentException if variable-length value does not terminate + * after 9 bytes have been read + * @see #writeUnsignedVarLong(long, DataOutput) + */ + public static long readUnsignedVarLong(DataInput in) throws IOException { + long value = 0L; + int i = 0; + long b; + while (((b = in.readByte()) & 0x80L) != 0) { + value |= (b & 0x7F) << i; + i += 7; + if (i > 63) { + throw new IllegalArgumentException("Variable length quantity is too long"); + } + } + return value | (b << i); + } + + /** + * @throws IllegalArgumentException if variable-length value does not terminate + * after 5 bytes have been read + * @throws IOException if {@link DataInput} throws {@link IOException} + * @see #readSignedVarLong(DataInput) + */ + public static int readSignedVarInt(DataInput in) throws IOException { + int raw = readUnsignedVarInt(in); + // This undoes the trick in writeSignedVarInt() + int temp = (((raw << 31) >> 31) ^ raw) >> 1; + // This extra step lets us deal with the largest signed values by treating + // negative results from read unsigned methods as like unsigned values. + // Must re-flip the top bit if the original read value had it set. + return temp ^ (raw & (1 << 31)); + } + + /** + * @throws IllegalArgumentException if variable-length value does not terminate + * after 5 bytes have been read + * @throws IOException if {@link DataInput} throws {@link IOException} + * @see #readUnsignedVarLong(DataInput) + */ + public static int readUnsignedVarInt(DataInput in) throws IOException { + int value = 0; + int i = 0; + int b; + while (((b = in.readByte()) & 0x80) != 0) { + value |= (b & 0x7F) << i; + i += 7; + if (i > 35) { + throw new IllegalArgumentException("Variable length quantity is too long"); + } + } + return value | (b << i); + } + + public static int readSignedVarInt(byte[] bytes) { + int raw = readUnsignedVarInt(bytes); + // This undoes the trick in writeSignedVarInt() + int temp = (((raw << 31) >> 31) ^ raw) >> 1; + // This extra step lets us deal with the largest signed values by treating + // negative results from read unsigned methods as like unsigned values. + // Must re-flip the top bit if the original read value had it set. + return temp ^ (raw & (1 << 31)); + } + + public static int readUnsignedVarInt(byte[] bytes) { + int value = 0; + int i = 0; + byte rb = Byte.MIN_VALUE; + for (byte b : bytes) { + rb = b; + if ((b & 0x80) == 0) { + break; + } + value |= (b & 0x7f) << i; + i += 7; + if (i > 35) { + throw new IllegalArgumentException("Variable length quantity is too long"); + } + } + return value | (rb << i); + } +} \ No newline at end of file diff --git a/src/main/java/com/luminiasoft/bitshares/errors/MalformedTransactionException.java b/src/main/java/com/luminiasoft/bitshares/errors/MalformedTransactionException.java new file mode 100644 index 0000000..604f8c4 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/errors/MalformedTransactionException.java @@ -0,0 +1,11 @@ +package com.luminiasoft.bitshares.errors; + +/** + * Created by nelson on 11/14/16. + */ +public class MalformedTransactionException extends Exception { + + public MalformedTransactionException(String message){ + super(message); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/interfaces/ByteSerializable.java b/src/main/java/com/luminiasoft/bitshares/interfaces/ByteSerializable.java new file mode 100644 index 0000000..b557013 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/interfaces/ByteSerializable.java @@ -0,0 +1,9 @@ +package com.luminiasoft.bitshares.interfaces; + +/** + * Interface implemented by all entities for which makes sense to have a byte-array representation. + */ +public interface ByteSerializable { + + byte[] toBytes(); +} diff --git a/src/main/java/com/luminiasoft/bitshares/interfaces/JsonSerializable.java b/src/main/java/com/luminiasoft/bitshares/interfaces/JsonSerializable.java new file mode 100644 index 0000000..78f8d29 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/interfaces/JsonSerializable.java @@ -0,0 +1,15 @@ +package com.luminiasoft.bitshares.interfaces; + +import com.google.gson.JsonElement; + +import java.io.Serializable; + +/** + * Interface to be implemented by any entity for which makes sense to have a JSON-formatted string representation. + */ +public interface JsonSerializable extends Serializable { + + String toJsonString(); + + JsonElement toJsonObject(); +} diff --git a/src/main/java/com/luminiasoft/bitshares/interfaces/WitnessResponseListener.java b/src/main/java/com/luminiasoft/bitshares/interfaces/WitnessResponseListener.java new file mode 100644 index 0000000..58560c0 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/interfaces/WitnessResponseListener.java @@ -0,0 +1,14 @@ +package com.luminiasoft.bitshares.interfaces; + +import com.luminiasoft.bitshares.models.BaseResponse; +import com.luminiasoft.bitshares.models.WitnessResponse; + +/** + * Class used to represent any listener to network requests. + */ +public interface WitnessResponseListener { + + void onSuccess(WitnessResponse response); + + void onError(BaseResponse.Error error); +} diff --git a/src/main/java/com/luminiasoft/bitshares/models/AccountProperties.java b/src/main/java/com/luminiasoft/bitshares/models/AccountProperties.java new file mode 100644 index 0000000..b57f0a2 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/models/AccountProperties.java @@ -0,0 +1,43 @@ +package com.luminiasoft.bitshares.models; + +/** + * Created by nelson on 11/15/16. + */ +public class AccountProperties { + public String id; + public String membership_expiration_date; + public String registrar; + public String referrer; + public String lifetime_referrer; + public long network_fee_percentage; + public long lifetime_referrer_fee_percentage; + public long referrer_rewards_percentage; + public String name; + public User owner; + public User active; + public Options options; + public String statistics; + public String[] whitelisting_accounts; + public String[] blacklisting_accounts; + public String[] whitelisted_accounts; + public String[] blacklisted_accounts; + public Object[] owner_special_authority; + public Object[] active_special_authority; + public long top_n_control_flags; + + class User { + public long weight_threshold; + public String[] account_auths; //TODO: Check this type + public String[][] key_auths; //TODO: Check how to deserialize this + public String[] address_auths; + } + + class Options { + public String memo_key; + public String voting_account; + public long num_witness; + public long num_committee; + public String[] votes; //TODO: Check this type + public String[] extensions; //TODO: Check this type + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java b/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java new file mode 100644 index 0000000..2b36736 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java @@ -0,0 +1,99 @@ +package com.luminiasoft.bitshares.models; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.luminiasoft.bitshares.interfaces.JsonSerializable; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * Class used to build a Graphene websocket API call. + * @see Websocket Calls & Notifications + */ +public class ApiCall implements JsonSerializable { + public static final String KEY_SEQUENCE_ID = "id"; + public static final String KEY_METHOD = "method"; + public static final String KEY_PARAMS = "params"; + public static final String KEY_JSON_RPC = "jsonrpc"; + + public String method; + public String methodToCall; + public String jsonrpc; + public List params; + public int apiId; + public int sequenceId; + + public ApiCall(int apiId, String methodToCall, List params, String jsonrpc, int sequenceId){ + this.apiId = apiId; + this.method = "call"; + this.methodToCall = methodToCall; + this.jsonrpc = jsonrpc; + this.params = params; + this.sequenceId = sequenceId; + } + + public ApiCall(int apiId, String method, String methodToCall, List params, String jsonrpc, int sequenceId){ + this.apiId = apiId; + this.method = method; + this.methodToCall = methodToCall; + this.jsonrpc = jsonrpc; + this.params = params; + this.sequenceId = sequenceId; + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(ApiCall.class, new ApiCallSerializer()); + return gsonBuilder.create().toJson(this); + } + + @Override + public JsonElement toJsonObject() { + JsonObject obj = new JsonObject(); + obj.addProperty(KEY_SEQUENCE_ID, this.sequenceId); + obj.addProperty(KEY_METHOD, this.method); + JsonArray paramsArray = new JsonArray(); + paramsArray.add(this.apiId); + paramsArray.add(this.methodToCall); + JsonArray methodParams = new JsonArray(); + + for(int i = 0; i < this.params.size(); i++){ + if(this.params.get(i) instanceof JsonSerializable){ + // Sometimes the parameters are objects + methodParams.add(((JsonSerializable) this.params.get(i)).toJsonObject()); + }else if(this.params.get(i) instanceof String || this.params.get(i) == null){ + // Other times they are plain strings + methodParams.add((String) this.params.get(i)); + }else if(this.params.get(i) instanceof ArrayList){ + // Other times it might be an array + JsonArray array = new JsonArray(); + ArrayList listArgument = (ArrayList) this.params.get(i); + for(int l = 0; l < listArgument.size(); l++){ + JsonSerializable s = listArgument.get(l); + array.add(s.toJsonObject()); + } + methodParams.add(array); + } + } + paramsArray.add(methodParams); + obj.add(KEY_PARAMS, paramsArray); + obj.addProperty(KEY_JSON_RPC, this.jsonrpc); + return obj; + } + + class ApiCallSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(ApiCall apiCall, Type type, JsonSerializationContext jsonSerializationContext) { + return toJsonObject(); + } + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/models/BaseResponse.java b/src/main/java/com/luminiasoft/bitshares/models/BaseResponse.java new file mode 100644 index 0000000..13df8f6 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/models/BaseResponse.java @@ -0,0 +1,29 @@ +package com.luminiasoft.bitshares.models; + +/** + * Created by nelson on 11/12/16. + */ +public class BaseResponse { + public int id; + public Error error; + + public static class Error { + public ErrorData data; + public int code; + public String message; + public Error(String message){ + this.message = message; + } + } + + public static class ErrorData { + public int code; + public String name; + public String message; + //TODO: Include stack data + + public ErrorData(String message){ + this.message = message; + } + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/models/DynamicGlobalProperties.java b/src/main/java/com/luminiasoft/bitshares/models/DynamicGlobalProperties.java new file mode 100644 index 0000000..c59278d --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/models/DynamicGlobalProperties.java @@ -0,0 +1,22 @@ +package com.luminiasoft.bitshares.models; + +/** + * Class used to deserialize the 'result' field returned by the full node after making a call + * to the 'get_dynamic_global_properties' RPC. + */ +public class DynamicGlobalProperties { + public String id; + public long head_block_number; + public String head_block_id; + public String time; + public String current_witness; + public String next_maintenance_time; + public String last_budget_time; + public long witness_budget; + public long accounts_registered_this_interval; + public long recently_missed_count; + public long current_aslot; + public String recent_slots_filled; + public long dynamic_flags; + public long last_irreversible_block_num; +} diff --git a/src/main/java/com/luminiasoft/bitshares/models/WitnessResponse.java b/src/main/java/com/luminiasoft/bitshares/models/WitnessResponse.java new file mode 100644 index 0000000..0ba8b26 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/models/WitnessResponse.java @@ -0,0 +1,8 @@ +package com.luminiasoft.bitshares.models; + +/** + * Generic witness response + */ +public class WitnessResponse extends BaseResponse{ + public T result; +} diff --git a/src/main/java/com/luminiasoft/bitshares/ws/GetAccountByName.java b/src/main/java/com/luminiasoft/bitshares/ws/GetAccountByName.java new file mode 100644 index 0000000..fc84df6 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/ws/GetAccountByName.java @@ -0,0 +1,71 @@ +package com.luminiasoft.bitshares.ws; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.luminiasoft.bitshares.RPC; +import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; +import com.luminiasoft.bitshares.models.AccountProperties; +import com.luminiasoft.bitshares.models.ApiCall; +import com.luminiasoft.bitshares.models.BaseResponse; +import com.luminiasoft.bitshares.models.WitnessResponse; +import com.neovisionaries.ws.client.WebSocket; +import com.neovisionaries.ws.client.WebSocketAdapter; +import com.neovisionaries.ws.client.WebSocketException; +import com.neovisionaries.ws.client.WebSocketFrame; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Created by nelson on 11/15/16. + */ +public class GetAccountByName extends WebSocketAdapter { + + private String accountName; + private WitnessResponseListener mListener; + + public GetAccountByName(String accountName, WitnessResponseListener listener){ + this.accountName = accountName; + this.mListener = listener; + } + + @Override + public void onConnected(WebSocket websocket, Map> headers) throws Exception { + ArrayList accountParams = new ArrayList<>(); + accountParams.add(this.accountName); + ApiCall getAccountByName = new ApiCall(0, RPC.CALL_GET_ACCOUNT_BY_NAME, accountParams, "2.0", 1); + websocket.sendText(getAccountByName.toJsonString()); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + String response = frame.getPayloadText(); + Gson gson = new Gson(); + + Type GetAccountByNameResponse = new TypeToken>(){}.getType(); + WitnessResponse> witnessResponse = gson.fromJson(response, GetAccountByNameResponse); + + if(witnessResponse.error != null){ + this.mListener.onError(witnessResponse.error); + }else{ + this.mListener.onSuccess(witnessResponse); + } + + websocket.disconnect(); + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + mListener.onError(new BaseResponse.Error(cause.getMessage())); + websocket.disconnect(); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + mListener.onError(new BaseResponse.Error(cause.getMessage())); + websocket.disconnect(); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/ws/GetRequiredFees.java b/src/main/java/com/luminiasoft/bitshares/ws/GetRequiredFees.java new file mode 100644 index 0000000..c4920f5 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/ws/GetRequiredFees.java @@ -0,0 +1,77 @@ +package com.luminiasoft.bitshares.ws; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.luminiasoft.bitshares.Asset; +import com.luminiasoft.bitshares.AssetAmount; +import com.luminiasoft.bitshares.BaseOperation; +import com.luminiasoft.bitshares.RPC; +import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; +import com.luminiasoft.bitshares.models.ApiCall; +import com.luminiasoft.bitshares.models.BaseResponse; +import com.luminiasoft.bitshares.models.WitnessResponse; +import com.neovisionaries.ws.client.WebSocket; +import com.neovisionaries.ws.client.WebSocketAdapter; +import com.neovisionaries.ws.client.WebSocketException; +import com.neovisionaries.ws.client.WebSocketFrame; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Created by nelson on 11/15/16. + */ +public class GetRequiredFees extends WebSocketAdapter { + + private WitnessResponseListener mListener; + private List operations; + private Asset asset; + + public GetRequiredFees(List operations, Asset asset, WitnessResponseListener listener){ + this.operations = operations; + this.asset = asset; + this.mListener = listener; + } + + @Override + public void onConnected(WebSocket websocket, Map> headers) throws Exception { + ArrayList accountParams = new ArrayList<>(); + accountParams.add((Serializable) this.operations); + accountParams.add(this.asset.getObjectId()); + ApiCall getRequiredFees = new ApiCall(0, RPC.CALL_GET_REQUIRED_FEES, accountParams, "2.0", 1); + websocket.sendText(getRequiredFees.toJsonString()); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + String response = frame.getPayloadText(); + Gson gson = new Gson(); + + Type GetRequiredFeesResponse = new TypeToken>>(){}.getType(); + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetDeserializer()); + WitnessResponse> witnessResponse = gsonBuilder.create().fromJson(response, GetRequiredFeesResponse); + + if(witnessResponse.error != null){ + mListener.onError(witnessResponse.error); + }else{ + mListener.onSuccess(witnessResponse); + } + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + mListener.onError(new BaseResponse.Error(cause.getMessage())); + websocket.disconnect(); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + mListener.onError(new BaseResponse.Error(cause.getMessage())); + websocket.disconnect(); + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java b/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java new file mode 100644 index 0000000..fa1a312 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java @@ -0,0 +1,176 @@ +package com.luminiasoft.bitshares.ws; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.luminiasoft.bitshares.BaseOperation; +import com.luminiasoft.bitshares.BlockData; +import com.luminiasoft.bitshares.RPC; +import com.luminiasoft.bitshares.Transaction; +import com.luminiasoft.bitshares.Transfer; +import com.luminiasoft.bitshares.TransferTransactionBuilder; +import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; +import com.luminiasoft.bitshares.models.ApiCall; +import com.luminiasoft.bitshares.models.BaseResponse; +import com.luminiasoft.bitshares.models.DynamicGlobalProperties; +import com.luminiasoft.bitshares.models.WitnessResponse; +import com.neovisionaries.ws.client.WebSocket; +import com.neovisionaries.ws.client.WebSocketAdapter; +import com.neovisionaries.ws.client.WebSocketException; +import com.neovisionaries.ws.client.WebSocketFrame; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +/** + * Class that will handle the transaction publication procedure. + */ +public class TransactionBroadcastSequence extends WebSocketAdapter { + private final String TAG = this.getClass().getName(); + + private final static int LOGIN_ID = 1; + private final static int GET_NETWORK_BROADCAST_ID = 2; + private final static int GET_NETWORK_DYNAMIC_PARAMETERS = 3; + private final static int BROADCAST_TRANSACTION = 4; + public final static int EXPIRATION_TIME = 30; + + private Transaction transaction; + private long expirationTime; + private String headBlockId; + private long headBlockNumber; + private WitnessResponseListener mListener; + + private int currentId = 1; + private int broadcastApiId = -1; + private int retries = 0; + + /** + * Constructor of this class. The ids required + * @param transaction: The transaction to be broadcasted. + * @param listener: A class implementing the WitnessResponseListener interface. This should + * be implemented by the party interested in being notified about the success/failure + * of the transaction broadcast operation. + */ + public TransactionBroadcastSequence(Transaction transaction, WitnessResponseListener listener){ + this.transaction = transaction; + this.mListener = listener; + } + + @Override + public void onConnected(WebSocket websocket, Map> headers) throws Exception { + ArrayList loginParams = new ArrayList<>(); + loginParams.add(null); + loginParams.add(null); + ApiCall loginCall = new ApiCall(1, RPC.CALL_LOGIN, loginParams, "2.0", currentId); + websocket.sendText(loginCall.toJsonString()); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + String response = frame.getPayloadText(); + Gson gson = new Gson(); + BaseResponse baseResponse = gson.fromJson(response, BaseResponse.class); + if(baseResponse.error != null && baseResponse.error.message.indexOf("is_canonical") == -1){ + mListener.onError(baseResponse.error); + websocket.disconnect(); + }else{ + currentId++; + ArrayList emptyParams = new ArrayList<>(); + if(baseResponse.id == LOGIN_ID){ + ApiCall networkApiIdCall = new ApiCall(1, RPC.CALL_NETWORK_BROADCAST, emptyParams, "2.0", currentId); + websocket.sendText(networkApiIdCall.toJsonString()); + }else if(baseResponse.id == GET_NETWORK_BROADCAST_ID){ + Type ApiIdResponse = new TypeToken>() {}.getType(); + WitnessResponse witnessResponse = gson.fromJson(response, ApiIdResponse); + broadcastApiId = witnessResponse.result; + + ApiCall getDynamicParametersCall = new ApiCall(0, RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES, emptyParams, "2.0", currentId); + websocket.sendText(getDynamicParametersCall.toJsonString()); + }else if(baseResponse.id == GET_NETWORK_DYNAMIC_PARAMETERS){ + Type DynamicGlobalPropertiesResponse = new TypeToken>(){}.getType(); + WitnessResponse witnessResponse = gson.fromJson(response, DynamicGlobalPropertiesResponse); + DynamicGlobalProperties dynamicProperties = witnessResponse.result; + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date date = dateFormat.parse(dynamicProperties.time); + + // Obtained block data + expirationTime = (date.getTime() / 1000) + EXPIRATION_TIME; + headBlockId = dynamicProperties.head_block_id; + headBlockNumber = dynamicProperties.head_block_number; + + ArrayList transactionList = new ArrayList<>(); + transactionList.add(transaction); + ApiCall call = new ApiCall(broadcastApiId, + RPC.CALL_BROADCAST_TRANSACTION, + transactionList, + "2.0", + currentId); + + // Finally sending transaction + websocket.sendText(call.toJsonString()); + }else if(baseResponse.id >= BROADCAST_TRANSACTION){ + Type WitnessResponseType = new TypeToken>(){}.getType(); + WitnessResponse> witnessResponse = gson.fromJson(response, WitnessResponseType); + if(witnessResponse.error == null){ + mListener.onSuccess(witnessResponse); + websocket.disconnect(); + }else{ + if(witnessResponse.error.message.indexOf("is_canonical") != -1 && retries < 10){ + /* + * This is a very ugly hack, but it will do for now. + * + * The issue is that the witness is complaining about the signature not + * being canonical even though the bitcoinj ECKey.ECDSASignature.isCanonical() + * method says it is! We'll have to dive deeper into this issue and avoid + * this error altogether + * + * But this MUST BE FIXED! Since this hack will only work for transactions + * with ONE transfer operation. + */ + retries++; + List operations = this.transaction.getOperations(); + Transfer transfer = (Transfer) operations.get(0); + transaction = new TransferTransactionBuilder() + .setSource(transfer.getFrom()) + .setDestination(transfer.getTo()) + .setAmount(transfer.getAmount()) + .setFee(transfer.getFee()) + .setBlockData(new BlockData(headBlockNumber, headBlockId, expirationTime + EXPIRATION_TIME)) + .setPrivateKey(transaction.getPrivateKey()) + .build(); + ArrayList transactionList = new ArrayList<>(); + transactionList.add(transaction); + ApiCall call = new ApiCall(broadcastApiId, + RPC.CALL_BROADCAST_TRANSACTION, + transactionList, + "2.0", + currentId); + websocket.sendText(call.toJsonString()); + }else{ + mListener.onError(witnessResponse.error); + websocket.disconnect(); + } + } + } + } + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + mListener.onError(new BaseResponse.Error(cause.getMessage())); + websocket.disconnect(); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + mListener.onError(new BaseResponse.Error(cause.getMessage())); + websocket.disconnect(); + } +} \ No newline at end of file