diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 1dde4f60b5..dbe604a60c 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/build.gradle b/app/build.gradle
index ec8de2db1a..d6bf56e099 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -44,6 +44,7 @@ android {
}
dependencies {
+ implementation project(':ripple-core')
implementation project(':card-common')
implementation project(':card-android')
implementation project(':server-android')
diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java
index e229b98330..31e89d72ee 100644
--- a/app/src/main/java/com/tangem/data/Blockchain.java
+++ b/app/src/main/java/com/tangem/data/Blockchain.java
@@ -12,12 +12,13 @@ public enum Blockchain {
Ethereum("ETH", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"),
EthereumTestNet("ETH/test", "ETH", 1.0, R.drawable.ic_logo_ethereum_testnet, "Ethereum Testnet"),
Token("Token", "ETH", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"),
- NftToken("NftToken", "", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"),
+ NftToken("NftToken", "", 1.0, R.drawable.tangem2, "Ethereum"),
BitcoinCash("BCH", "BCH", 100000000.0, R.drawable.ic_logo_bitcoin_cash, "Bitcoin Cash"),
- Litecoin("LTC", "LTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Litecoin"),
- Rootstock("RSK", "RBTC", 1.0, R.drawable.ic_logo_bitcoin, "Rootstock"),
- RootstockToken("Token", "RBTC", 1.0, R.drawable.ic_logo_bat_token, "Rootstock"),
- Cardano("CARDANO", "ADA", 1000000.0,R.drawable.ic_logo_bitcoin, "Cardano");
+ Litecoin("LTC", "LTC", 100000000.0, R.drawable.tangem2, "Litecoin"),
+ Rootstock("RSK", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"),
+ RootstockToken("Token", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"),
+ Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"),
+ Ripple ("XRP", "XRP", 1000000.0, R.drawable.tangem2, "Ripple");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;
@@ -92,6 +93,9 @@ public enum Blockchain {
case "BTC":
return R.drawable.ic_logo_bitcoin;
+ case "BCH":
+ return R.drawable.ic_logo_bitcoin_cash;
+
case "Token":
if (symbolName.equals("SEED"))
return R.drawable.ic_logo_seed;
diff --git a/app/src/main/java/com/tangem/data/network/RippleApi.java b/app/src/main/java/com/tangem/data/network/RippleApi.java
new file mode 100644
index 0000000000..1d457ad859
--- /dev/null
+++ b/app/src/main/java/com/tangem/data/network/RippleApi.java
@@ -0,0 +1,15 @@
+package com.tangem.data.network;
+
+import com.tangem.data.network.model.RippleBody;
+import com.tangem.data.network.model.RippleResponse;
+
+import retrofit2.Call;
+import retrofit2.http.Body;
+import retrofit2.http.Headers;
+import retrofit2.http.POST;
+
+public interface RippleApi {
+ @Headers("Content-Type: application/json")
+ @POST
+ Call ripple(@Body RippleBody body);
+}
diff --git a/app/src/main/java/com/tangem/data/network/ServerApiRipple.java b/app/src/main/java/com/tangem/data/network/ServerApiRipple.java
new file mode 100644
index 0000000000..ba5c356b47
--- /dev/null
+++ b/app/src/main/java/com/tangem/data/network/ServerApiRipple.java
@@ -0,0 +1,114 @@
+package com.tangem.data.network;
+
+import android.util.Log;
+
+import com.tangem.App;
+import com.tangem.data.network.model.RippleBody;
+import com.tangem.data.network.model.RippleResponse;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+public class ServerApiRipple {
+ private static String TAG = ServerApiRipple.class.getSimpleName();
+
+ public static final String RIPPLE_ACCOUNT_INFO = "account_info";
+ public static final String RIPPLE_ACCOUNT_UNCONFIRMED = "account_unconfirmed";
+ public static final String RIPPLE_SUBMIT = "submit";
+ public static final String RIPPLE_FEE = "fee";
+
+ private int requestsCount=0;
+
+ public static String lastNode;
+
+ public boolean isRequestsSequenceCompleted() {
+ Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount));
+ return requestsCount <= 0;
+ }
+
+ private ResponseListener responseListener;
+
+ public interface ResponseListener {
+ void onSuccess(String method, RippleResponse infuraResponse);
+
+ void onFail(String method, String message);
+ }
+
+ public void setResponseListener(ResponseListener listener) {
+ responseListener = listener;
+ }
+
+ public void requestData(String method, int id, String wallet, String tx) {
+ requestsCount++;
+ String rippleURL = "http://s1.ripple.com:51234/"; //TODO: make random selection
+ lastNode = rippleURL; //TODO: show node instead of URL
+
+ Retrofit retrofitRipple = new Retrofit.Builder()
+ .baseUrl(rippleURL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build();
+
+ RippleApi rippleApi = retrofitRipple.create(RippleApi.class);
+
+ RippleBody rippleBody;
+ HashMap paramsMap;
+
+ switch (method) {
+ case RIPPLE_ACCOUNT_INFO:
+ paramsMap = new HashMap<>();
+ paramsMap.put("account", wallet);
+ paramsMap.put("ledger_index", "validated");
+ rippleBody = new RippleBody(method, paramsMap);
+ break;
+
+ case RIPPLE_ACCOUNT_UNCONFIRMED:
+ paramsMap = new HashMap<>();
+ paramsMap.put("account", wallet);
+ paramsMap.put("ledger_index", "current");
+// paramsMap.put("queue", "true"); TODO: make queue check if needed
+ rippleBody = new RippleBody(RIPPLE_ACCOUNT_INFO, paramsMap);
+ break;
+
+ case RIPPLE_FEE:
+ rippleBody = new RippleBody(method, new HashMap<>());
+ break;
+
+ case RIPPLE_SUBMIT:
+ paramsMap = new HashMap<>();
+ paramsMap.put("tx_blob", tx);
+ rippleBody = new RippleBody(method, paramsMap);
+ break;
+
+ default:
+ rippleBody = new RippleBody();
+ }
+
+ Call call = rippleApi.ripple(rippleBody);
+ call.enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.code() == 200) {
+ requestsCount--;
+ responseListener.onSuccess(method, response.body());
+ Log.i(TAG, "requestData " + method + " onResponse " + response.code());
+ } else {
+ responseListener.onFail(method, String.valueOf(response.code()));
+ Log.e(TAG, "requestData " + method + " onResponse " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ responseListener.onFail(method, String.valueOf(t.getMessage()));
+ Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage());
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/tangem/data/network/model/RippleBody.java b/app/src/main/java/com/tangem/data/network/model/RippleBody.java
new file mode 100644
index 0000000000..0423f357a1
--- /dev/null
+++ b/app/src/main/java/com/tangem/data/network/model/RippleBody.java
@@ -0,0 +1,24 @@
+package com.tangem.data.network.model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class RippleBody {
+ private String method;
+ private ArrayList> params;
+
+ public RippleBody() {
+ }
+
+ //for RIPPLE_FEE
+ public RippleBody(String method) {
+ this.method = method;
+ }
+
+ public RippleBody(String method, HashMap paramsMap) {
+ this.method = method;
+ ArrayList> paramsList = new ArrayList<>();
+ paramsList.add(paramsMap);
+ this.params = paramsList;
+ }
+}
diff --git a/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt b/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt
new file mode 100644
index 0000000000..6c61b6abb0
--- /dev/null
+++ b/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt
@@ -0,0 +1,52 @@
+package com.tangem.data.network.model
+
+import com.google.gson.annotations.SerializedName
+
+data class RippleResponse(
+ @SerializedName("result")
+ var result: RippleResult? = null
+)
+
+data class RippleResult(
+ @SerializedName("account_data")
+ var account_data: RippleAccountData? = null,
+
+ @SerializedName("validated")
+ var validated: Boolean? = null,
+
+ //for RIPPLE_FEE
+ @SerializedName("drops")
+ var drops: FeeDrops? = null,
+
+ //for RIPPLE_SUBMIT
+ @SerializedName("engine_result")
+ var engine_result: String? = null,
+
+ //for RIPPLE_SUBMIT
+ @SerializedName ("engine_result_message")
+ var engine_result_message: String? = null
+)
+
+data class RippleAccountData(
+ @SerializedName("Account")
+ var account: String? = null,
+
+ @SerializedName("Balance")
+ var balance: String? = null,
+
+ @SerializedName("Sequence")
+ var sequence: Int? = null
+)
+
+data class FeeDrops(
+ //enough to put tx to queue
+ @SerializedName("minimum_fee")
+ var minimum_fee: String? = null,
+
+ //enough to put tx to current ledger
+ @SerializedName("open_ledger_fee")
+ var open_ledger_fee: String? = null,
+
+ @SerializedName("median_fee")
+ var median_fee: String? = null
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java
new file mode 100644
index 0000000000..7a34b0d289
--- /dev/null
+++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java
@@ -0,0 +1,115 @@
+package com.tangem.domain.wallet.xrp;
+
+import java.math.BigInteger;
+
+/**
+ * Created by Ilia on 15.02.2018.
+ */
+
+public class XrpBase58 {
+ private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray();
+
+ private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long
+ private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS
+ private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1,
+ -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1,
+ -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46,
+ 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
+
+ public static byte[] decodeBase58(String input) {
+ if (input == null) {
+ return null;
+ }
+ input = input.trim();
+ if (input.length() == 0) {
+ return new byte[0];
+ }
+ BigInteger resultNum = BigInteger.ZERO;
+ int nLeadingZeros = 0;
+ while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) {
+ nLeadingZeros++;
+ }
+ long acc = 0;
+ int nDigits = 0;
+ int p = nLeadingZeros;
+ while (p < input.length()) {
+ int v = BASE58_VALUES[input.charAt(p) & 0xff];
+ if (v >= 0) {
+ acc *= 58;
+ acc += v;
+ nDigits++;
+ if (nDigits == BASE58_CHUNK_DIGITS) {
+ resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc));
+ acc = 0;
+ nDigits = 0;
+ }
+ p++;
+ } else {
+ break;
+ }
+ }
+ if (nDigits > 0) {
+ long mul = 58;
+ while (--nDigits > 0) {
+ mul *= 58;
+ }
+ resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc));
+ }
+ final int BASE58_SPACE = -2;
+ while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) {
+ p++;
+ }
+ if (p < input.length()) {
+ return null;
+ }
+ byte[] plainNumber = resultNum.toByteArray();
+ int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0;
+ byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs];
+ System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs);
+ return result;
+ }
+
+ public static String encodeBase58(byte[] input) {
+ if (input == null) {
+ return null;
+ }
+ StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1);
+ BigInteger bn = new BigInteger(1, input);
+ long rem;
+ while (true) {
+ BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD);
+ bn = divideAndRemainder[0];
+ rem = divideAndRemainder[1].longValue();
+ if (bn.compareTo(BigInteger.ZERO) == 0) {
+ break;
+ }
+ for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) {
+ str.append(BASE58[(int) (rem % 58)]);
+ rem /= 58;
+ }
+ }
+ while (rem != 0) {
+ str.append(BASE58[(int) (rem % 58)]);
+ rem /= 58;
+ }
+ str.reverse();
+ int nLeadingZeros = 0;
+ while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) {
+ str.insert(0, BASE58[0]);
+ nLeadingZeros++;
+ }
+ return str.toString();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java
new file mode 100644
index 0000000000..64613e1605
--- /dev/null
+++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java
@@ -0,0 +1,78 @@
+package com.tangem.domain.wallet.xrp;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import com.tangem.domain.wallet.CoinData;
+import com.tangem.domain.wallet.CoinEngine;
+
+import java.math.BigDecimal;
+
+public class XrpData extends CoinData {
+ public XrpData() {
+ super();
+ }
+
+ private Long balanceConfirmed, balanceUnconfirmed, sequence;
+
+ @Override
+ public void loadFromBundle(Bundle B) {
+ super.loadFromBundle(B);
+
+ if (B.containsKey("BalanceConfirmed")) balanceConfirmed = B.getLong("BalanceConfirmed");
+ else balanceConfirmed = null;
+ if (B.containsKey("BalanceUnconfirmed")) balanceUnconfirmed = B.getLong("BalanceUnconfirmed");
+ else balanceUnconfirmed = null;
+ if (B.containsKey("Sequence")) sequence = B.getLong("Sequence");
+ }
+
+ @Override
+ public void saveToBundle(Bundle B) {
+ super.saveToBundle(B);
+ try {
+ if (balanceConfirmed != null) B.putLong("BalanceConfirmed", balanceConfirmed);
+ if (balanceUnconfirmed != null) B.putLong("BalanceUnconfirmed", balanceUnconfirmed);
+ if (sequence != null) B.putLong("Sequence", sequence);
+ } catch (Exception e) {
+ Log.e("Can't save to bundle ", e.getMessage());
+ }
+ }
+
+ @Override
+ public void clearInfo() {
+ super.clearInfo();
+ balanceConfirmed = null;
+ balanceUnconfirmed = null;
+ sequence = null;
+ }
+
+ // balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present
+ public CoinEngine.InternalAmount getBalanceInInternalUnits() {
+ return new CoinEngine.InternalAmount(BigDecimal.valueOf(balanceUnconfirmed), "Drops");
+ }
+
+// public Long getBalanceUnconfirmed() {
+// return balanceUnconfirmed;
+// }
+
+ public void setBalanceConfirmed(Long balance) {
+ this.balanceConfirmed = balance;
+ }
+
+ public void setBalanceUnconfirmed(Long balance) {
+ this.balanceUnconfirmed = balance;
+ }
+
+ public void setSequence(Long sequence) {
+ this.sequence = sequence;
+ }
+
+ public boolean hasBalanceInfo() {
+ return balanceConfirmed != null || balanceUnconfirmed != null;
+ }
+
+ public boolean hasUnconfirmed() {
+ return !balanceConfirmed.equals(balanceUnconfirmed);
+ }
+
+}
diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java
new file mode 100644
index 0000000000..53ef3e3162
--- /dev/null
+++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java
@@ -0,0 +1,411 @@
+package com.tangem.domain.wallet.xrp;
+
+import android.net.Uri;
+import android.text.InputFilter;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.types.known.tx.signed.SignedTransaction;
+import com.ripple.core.types.known.tx.txns.Payment;
+import com.tangem.card_common.data.TangemCard;
+import com.tangem.card_common.reader.CardProtocol;
+import com.tangem.card_common.tasks.SignTask;
+import com.tangem.card_common.util.Util;
+import com.tangem.domain.wallet.BTCUtils;
+import com.tangem.domain.wallet.BalanceValidator;
+import com.tangem.domain.wallet.CoinData;
+import com.tangem.domain.wallet.CoinEngine;
+import com.tangem.domain.wallet.TangemContext;
+import com.tangem.util.CryptoUtil;
+import com.tangem.util.DecimalDigitsInputFilter;
+import com.tangem.wallet.R;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.Arrays;
+
+public class XrpEngine extends CoinEngine {
+
+ private static final String TAG = XrpEngine.class.getSimpleName();
+
+ public XrpData coinData = null;
+
+ public XrpEngine(TangemContext context) throws Exception {
+ super(context);
+ if (context.getCoinData() == null) {
+ coinData = new XrpData();
+ context.setCoinData(coinData);
+ } else if (context.getCoinData() instanceof XrpData) {
+ coinData = (XrpData) context.getCoinData();
+ } else {
+ throw new Exception("Invalid type of Blockchain data for XrpEngine");
+ }
+ }
+
+ public XrpEngine() {
+ super();
+ }
+
+ private static int getDecimals() {
+ return 6;
+ }
+
+ private void checkBlockchainDataExists() throws Exception {
+ if (coinData == null) throw new Exception("No blockchain data");
+ }
+
+ @Override
+ public boolean awaitingConfirmation() {
+ if (coinData == null) return false;
+ return coinData.hasUnconfirmed();
+ }
+
+ @Override
+ public String getBalanceHTML() {
+ Amount balance = getBalance();
+ if (balance != null) {
+ return balance.toDescriptionString(getDecimals());
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public String getBalanceCurrency() {
+ return "XRP";
+ }
+
+ @Override
+ public String getOfflineBalanceHTML() {
+ InternalAmount offlineInternalAmount = convertToInternalAmount(ctx.getCard().getOfflineBalance());
+ Amount offlineAmount = convertToAmount(offlineInternalAmount);
+ return offlineAmount.toDescriptionString(getDecimals());
+ }
+
+ @Override
+ public boolean isBalanceNotZero() {
+ if (coinData == null) return false;
+ if (coinData.getBalanceInInternalUnits() == null) return false;
+ return coinData.getBalanceInInternalUnits().notZero();
+ }
+
+ @Override
+ public boolean hasBalanceInfo() {
+ if (coinData == null) return false;
+ return coinData.hasBalanceInfo();
+ }
+
+ public boolean isExtractPossible() {
+ if (!hasBalanceInfo()) {
+ ctx.setMessage(R.string.cannot_obtain_data_from_blockchain);
+ } else if (!isBalanceNotZero()) {
+ ctx.setMessage(R.string.wallet_empty);
+ } else if (awaitingConfirmation()) {
+ ctx.setMessage(R.string.please_wait_while_previous);
+ } else {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String getFeeCurrency() {
+ return "XRP";
+ }
+
+ @Override
+ public boolean validateAddress(String address) {
+ if (address == null || address.isEmpty()) {
+ return false;
+ }
+
+ if (address.length() < 25) {
+ return false;
+ }
+
+ if (address.length() > 35) {
+ return false;
+ }
+
+ if (!address.startsWith("r")) {
+ return false;
+ }
+
+ byte[] decAddress = XrpBase58.decodeBase58(address);
+
+ if (decAddress == null || decAddress.length == 0) {
+ return false;
+ }
+
+ byte[] payload = new byte[21];
+ System.arraycopy(decAddress, 0, payload, 0, 21);
+
+ byte[] checksum = new byte[4];
+ System.arraycopy(decAddress, 21, checksum, 0, 4);
+
+ byte[] calcChecksum = new byte[4];
+ System.arraycopy(CryptoUtil.doubleSha256(payload),0,calcChecksum,0,4);
+
+ if (!Arrays.equals(checksum, calcChecksum)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean isNeedCheckNode() {
+ return true;
+ }
+
+ @Override
+ public Uri getWalletExplorerUri() {
+ return Uri.parse("https://xrpscan.com/account/" + ctx.getCoinData().getWallet());
+ }
+
+ @Override
+ public InputFilter[] getAmountInputFilters() {
+ return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())};
+ }
+
+ @Override
+ public boolean checkNewTransactionAmount(Amount amount) {
+ if (coinData == null) return false;
+ if (amount.compareTo(convertToAmount(coinData.getBalanceInInternalUnits())) > 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean checkNewTransactionAmountAndFee(Amount amountValue, Amount feeValue, Boolean isIncludeFee) {
+ InternalAmount fee;
+ InternalAmount amount;
+
+ try {
+ checkBlockchainDataExists();
+ amount = convertToInternalAmount(amountValue);
+ fee = convertToInternalAmount(feeValue);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ if (fee == null || amount == null)
+ return false;
+
+ if (fee.isZero() || amount.isZero())
+ return false;
+
+ if (isIncludeFee && (amount.compareTo(coinData.getBalanceInInternalUnits()) > 0 || amount.compareTo(fee) < 0))
+ return false;
+
+ if (!isIncludeFee && amount.add(fee).compareTo(coinData.getBalanceInInternalUnits()) > 0)
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public boolean validateBalance(BalanceValidator balanceValidator) {
+ try {
+ if (((ctx.getCard().getOfflineBalance() == null) && !ctx.getCoinData().isBalanceReceived()) || (!ctx.getCoinData().isBalanceReceived() && (ctx.getCard().getRemainingSignatures() != ctx.getCard().getMaxSignatures()))) {
+ balanceValidator.setScore(0);
+ balanceValidator.setFirstLine("Unknown balance");
+ balanceValidator.setSecondLine("Balance cannot be verified. Swipe down to refresh.");
+ return false;
+ }
+
+ if (coinData.hasUnconfirmed()) {
+ balanceValidator.setScore(0);
+ balanceValidator.setFirstLine("Transaction in progress");
+ balanceValidator.setSecondLine("Wait for confirmation in blockchain");
+ return false;
+ }
+
+ if (coinData.isBalanceReceived()) {// && coinData.isBalanceEqual()) { TODO:check
+ balanceValidator.setScore(100);
+ balanceValidator.setFirstLine("Verified balance");
+ balanceValidator.setSecondLine("Balance confirmed in blockchain");
+ if (coinData.getBalanceInInternalUnits().isZero()) {
+ balanceValidator.setFirstLine("Empty wallet");
+ balanceValidator.setSecondLine("");
+ }
+ }
+
+ if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
+ balanceValidator.setScore(80);
+ balanceValidator.setFirstLine("Verified offline balance");
+ balanceValidator.setSecondLine("Can't obtain balance from blockchain. Restore internet connection to be more confident. ");
+ }
+
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ public Amount getBalance() {
+ if (!hasBalanceInfo()) return null;
+ return convertToAmount(coinData.getBalanceInInternalUnits());
+ }
+
+ @Override
+ public String evaluateFeeEquivalent(String fee) {
+ if (!coinData.getAmountEquivalentDescriptionAvailable()) return "";
+ try {
+ Amount feeAmount = new Amount(fee, getFeeCurrency());
+ return feeAmount.toEquivalentString(coinData.getRate());
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ @Override
+ public String getBalanceEquivalent() {
+ if (coinData == null || !coinData.getAmountEquivalentDescriptionAvailable()) return "";
+ Amount balance = getBalance();
+ if (balance == null) return "";
+ return balance.toEquivalentString(coinData.getRate());
+ }
+
+ public String calculateAddress(byte[] pkCompressed) throws NoSuchAlgorithmException, NoSuchProviderException, IOException {
+ byte[] pkForHash = canonicalPubBytes(pkCompressed);
+
+ ByteArrayOutputStream address = new ByteArrayOutputStream();
+ address.write((byte) 0x00);
+ byte[] accountId = CryptoUtil.sha256ripemd160(pkForHash);
+ address.write(accountId);
+
+ byte [] doubleSha256 = CryptoUtil.doubleSha256(address.toByteArray());
+ address.write(Arrays.copyOfRange(doubleSha256,0,4));
+
+ return BTCUtils.toHex(address.toByteArray());
+ }
+
+ @Override
+ public Amount convertToAmount(InternalAmount internalAmount) {
+ BigDecimal d = internalAmount.divide(new BigDecimal("1000000"));
+ return new Amount(d, getBalanceCurrency());
+ }
+
+ @Override
+ public Amount convertToAmount(String strAmount, String currency) {
+ return new Amount(strAmount, currency);
+ }
+
+ @Override
+ public InternalAmount convertToInternalAmount(Amount amount) {
+ BigDecimal d = amount.multiply(new BigDecimal("1000000"));
+ return new InternalAmount(d, "Drops");
+ }
+
+ @Override
+ public InternalAmount convertToInternalAmount(byte[] bytes) {
+ if (bytes == null) return null;
+ byte[] reversed = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1];
+ return new InternalAmount(Util.byteArrayToLong(reversed), "Drops");
+ }
+
+ @Override
+ public byte[] convertToByteArray(InternalAmount internalAmount) {
+ byte[] bytes = Util.longToByteArray(internalAmount.longValueExact());
+// byte[] reversed = new byte[bytes.length]; TODO: check if needed
+// for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1];
+// return reversed;
+ return bytes;
+ }
+
+ @Override
+ public CoinData createCoinData() {
+ return new XrpData();
+ }
+
+ @Override
+ public String getUnspentInputsDescription() {
+ return "";
+ }
+
+ @Override
+ public void defineWallet() throws CardProtocol.TangemException {
+ try {
+ String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar());
+ ctx.getCoinData().setWallet(wallet);
+ }
+ catch (Exception e)
+ {
+ ctx.getCoinData().setWallet("ERROR");
+ throw new CardProtocol.TangemException("Can't define wallet address");
+ }
+ }
+
+ public byte[] canonicalPubBytes(byte[] pkCompressed) {
+ byte[] pkForHash = new byte[33];
+
+ if (pkCompressed.length == 32) {
+ pkForHash[0] = (byte) 0xED;
+ System.arraycopy(pkCompressed, 0, pkForHash, 1,32);
+ } else {
+ pkForHash = pkCompressed;
+ }
+ }
+}
+
+ @Override
+ public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
+ checkBlockchainDataExists();
+
+ Payment payment = new Payment();
+
+ // Put `as` AccountID field Account, `Object` o
+ payment.as(AccountID.Account, "rGZG674DSZJfoY8abMPSgChxZTJZEhyMRm");
+ payment.as(AccountID.Destination, "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK");
+ payment.as(com.ripple.core.coretypes.Amount.Amount, "1000000000");
+ payment.as(UInt32.Sequence, 10);
+ payment.as(com.ripple.core.coretypes.Amount.Fee, "10000");
+
+ SignedTransaction signedTx = payment.prepare(canonicalPubBytes(ctx.getCard().getWalletPublicKeyRar()));
+
+ return new SignTask.TransactionToSign() {
+
+ @Override
+ public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
+ return signingMethod == TangemCard.SigningMethod.Sign_Hash;
+ }
+
+ @Override
+ public byte[][] getHashesToSign() {
+ byte[][] dataForSign = new byte[1][];
+ dataForSign[0] = signedTx.signingData;
+ return dataForSign;
+ }
+
+ @Override
+ public byte[] getRawDataToSign() throws Exception {
+ throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName());
+ }
+
+ @Override
+ public String getHashAlgToSign() throws Exception {
+ throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName());
+ }
+
+ @Override
+ public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception {
+ throw new Exception("Transaction validation by issuer not supported in this version");
+ }
+
+ @Override
+ public byte[] onSignCompleted(byte[] signFromCard) {
+ signedTx.addSign(signFromCard);
+ return BTCUtils.fromHex(signedTx.tx_blob);
+ }
+ };
+ }
+}
diff --git a/ripple-core/.gitignore b/ripple-core/.gitignore
new file mode 100644
index 0000000000..ea8c4bf7f3
--- /dev/null
+++ b/ripple-core/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/ripple-core/README.md b/ripple-core/README.md
new file mode 100644
index 0000000000..d89d96f5e8
--- /dev/null
+++ b/ripple-core/README.md
@@ -0,0 +1,391 @@
+# SerializedTypes Overview
+
+Ripple uses a node store, where objects are keyed by a 32byte hash value.
+
+These keys are created by hashing a binary representation of the whole object
+prefixed with a name-spacing sequence of bytes, unique to each class of object.
+As such, a method of consistently producing a binary sequence from a given
+object was required.
+
+Objects have fields, which are a pairing of a type and a name. Names are simply
+ordinals used to look up preassigned names in a per type table. The type ordinal,
+similarly, is used to lookup a given class.
+
+In ripple-lib-java the type equates to a `SerializedType`, with types such as 32
+bit unsigned integer, variable length byte strings and even arrays of objects.
+The fields have an ordinal quality, and can be deterministically sorted,
+important for consistent binary representation.
+
+There are also container types: An STObject, short for `SerializedType Object`,
+is an associative container of fields to other SerializedTypes (even an STObject
+itself). An STArray is an array of STObjects with a single Field, mapped to an
+STObject, containing an arbitrary amount of Field -> SerializedType pairs. (See
+notes below on differences to C++ implementation)
+
+## Types
+
+Following is a survey of some of the classes, sorted somewhat topologically.
+
+The following json representation of metadata related to a finalized transaction
+can be used as a concrete example and will be referenced later.
+
+```java
+/**
+ *
+ * # Transaction Meta JSON
+ *
+ * {
+ * "TransactionResult": "tesSUCCESS",
+ *
+ * {@link com.ripple.core.types.known.tx.result.TransactionResult}
+ * {@link com.ripple.core.fields.Field.TransactionResult}
+ * {@link com.ripple.core.coretypes.uint.UInt8}
+ *
+ * "TransactionIndex": 0,
+ *
+ * {@link com.ripple.core.types.known.tx.result.TransactionMeta#transactionIndex}
+ * {@link com.ripple.core.coretypes.uint.UInt32}
+ *
+ * "AffectedNodes": [
+ *
+ * {@link com.ripple.core.types.known.tx.result.TransactionMeta#affectedNodes}
+ *
+ * {
+
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+
+ *
+ * "NewFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "Sequence": 103929,
+ *
+ * {@link com.ripple.core.fields.Field#Sequence}
+ *
+ *
+ * "TakerGets": {
+ *
+ * {@link com.ripple.core.coretypes.Amount}
+ *
+ * "currency": "ILS",
+ *
+ * {@link com.ripple.core.coretypes.Currency}
+ *
+ * "value": "1694.768",
+ *
+ * {@link com.ripple.core.coretypes.AccountID}
+ *
+ * "issuer": "rNPRNzBB92BVpAhhZr4iXDTveCgV5Pofm9"
+ *
+ * {@link java.math.BigDecimal}
+ *
+ * },
+ *
+ *
+ * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
+ *
+ * {@link com.ripple.core.coretypes.AccountID}
+ *
+ * "BookDirectory": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000",
+ *
+ * {@link com.ripple.core.coretypes.Quality#fromBookDirectory}
+ *
+ * "TakerPays": "98957503520",
+ *
+ * {@link com.ripple.core.coretypes.Amount}
+ * {@link com.ripple.core.coretypes.Amount#isNative()}
+ *
+ * "OwnerNode": "000000000000000E"
+ *
+ * {@link com.ripple.core.types.known.sle.entries.Offer#ownerNodeDirectoryIndex}
+ * },
+ *
+ * "LedgerIndex": "3596CE72C902BAFAAB56CC486ACAF9B4AFC67CF7CADBB81A4AA9CBDC8C5CB1AA",
+ *
+ * {@link com.ripple.core.coretypes.hash.Hash256#index}
+ *
+ * "LedgerEntryType": "Offer"
+ *
+ * {@link com.ripple.core.serialized.enums.LedgerEntryType}
+ * {@link com.ripple.core.types.known.sle.entries.Offer}
+ * }
+ * },
+ *
+ *
+ * {
+
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+
+ *
+ * "NewFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "RootIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000",
+ *
+ * com.ripple.core.coretypes.hash.Hash256#index
+ *
+ * "TakerGetsIssuer": "92D705968936C419CE614BF264B5EEB1CEA47FF4",
+ *
+ * {@link com.ripple.core.coretypes.Currency}
+ * {@link com.ripple.core.coretypes.hash.Hash160}
+ * {@link com.ripple.core.fields.Field#TakerGetsIssuer}
+ *
+ * "ExchangeRate": "5C14BE8A20D7F000",
+ *
+ * {@link com.ripple.core.coretypes.Quality#fromBookDirectory}
+ *
+ * "TakerGetsCurrency": "000000000000000000000000494C530000000000"
+ *
+ * {@link com.ripple.core.coretypes.Currency}
+ * {@link com.ripple.core.coretypes.hash.Hash160}
+ * {@link com.ripple.core.fields.Field#TakerGetsCurrency}
+ *
+ * },
+ *
+ * "LedgerIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000",
+ *
+ * {@link com.ripple.core.coretypes.hash.Hash256#index}
+ *
+ * "LedgerEntryType": "DirectoryNode"
+ *
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode}
+ * {@link com.ripple.core.serialized.enums.LedgerEntryType#DirectoryNode}
+ * }
+ * },
+ *
+ * {
+ * "ModifiedNode": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()}
+ *
+ * "FinalFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "RootIndex": "801C5AFB5862D4666D0DF8E5BE1385DC9B421ED09A4269542A07BC0267584B64",
+ *
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#rootIndex}
+ *
+ * "Flags": 0,
+ * "Owner": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
+ *
+ * {@link com.ripple.core.coretypes.hash.Index#ownerDirectory}
+ *
+ * "IndexPrevious": "0000000000000000"
+ *
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#hasPreviousIndex}
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#prevIndex}
+ * },
+ *
+ * "LedgerIndex": "AB03F8AA02FFA4635E7CE2850416AEC5542910A2B4DBE93C318FEB08375E0DB5",
+ *
+ * "LedgerEntryType": "DirectoryNode"
+ * }
+ * },
+ * {
+ * "ModifiedNode": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()}
+ *
+ * "FinalFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "Sequence": 103930,
+ * "Flags": 0,
+ * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
+ * "OwnerCount": 9,
+ * "Balance": "106861218302"
+ *
+ * {@link com.ripple.core.coretypes.Amount}
+ * },
+ *
+ * "LedgerIndex": "CF23A37E39A571A0F22EC3E97EB0169936B520C3088963F16C5EE4AC59130B1B",
+ *
+ * {@link com.ripple.core.coretypes.hash.Index#accountRoot}
+ *
+ * "LedgerEntryType": "AccountRoot",
+ *
+ * {@link com.ripple.core.types.known.sle.entries.AccountRoot}
+ * {@link com.ripple.core.serialized.enums.LedgerEntryType#AccountRoot}
+ *
+ * "PreviousFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsPrevious()}
+ *
+ * "Sequence": 103929,
+ * "OwnerCount": 8,
+ * "Balance": "106861218312"
+ * },
+ *
+ * "PreviousTxnID": "DE15F43F4A73C4F6CB1C334D9E47BDE84467C0902796BB81D4924885D1C11E6D",
+ *
+ * {@link com.ripple.core.types.known.tx.Transaction#hash}
+ *
+ * "PreviousTxnLgrSeq": 3225338
+ *
+ * {@link com.ripple.core.coretypes.uint.UInt32}
+ * }
+ * }
+ * ]
+ * }
+*/
+```
+
+* Note that ALL field names are, by convention, upper case (in fact field names (index, hash) may be
+lower cased but are not serialized)
+
+* The `AffectedNodes` is an STArray. As stated, the immediate
+children each contain only a single key (or [Field](src/main/java/com/ripple/core/fields/Field.java#L138-L140))
+
+ * ModifiedNode
+
+Moving on.
+
+```
+com
+└── ripple
+ ├── serialized
+ ├── core
+ │
+ ├── enums
+ │ ├── LedgerEntryType
+ │ ├── EngineResult
+ │ └── TransactionType
+```
+
+* In the json above look at the [TransactionResult](src/main/java/com/ripple/core/fields/Field.java#L164) field.
+ Note that it has a Type of of UINT8, yet clearly it's represented in json as a string.
+
+#### com.ripple.core.fields.Type
+
+ This is a simple Java enum(eration) of the various types. eg.
+
+ UINT32(2)
+
+ This definition implies giving the static ordinal `2` to the UINT32 type.
+
+#### com.ripple.core.fields.Field
+
+ Consider the following definition of a field
+
+ QualityIn(20, Type.UINT32)
+
+ As stated before a Field has name and type ordinals, but it also has an
+ implied symbolic string representation (as seen used in the json above)
+
+ The string name can be looked up via a `code`, which is an integer created by
+ shifting the type ordinal 16 bits to the left and ORing it with the name.
+
+ See: [com.ripple.core.fields.Field#fromCode](src/main/java/com/ripple/core/fields/Field.java)
+
+#### com.ripple.core.fields.HasField
+
+ This is simply an interface for returning a Field. We know that a Field
+ implies a Type and a name and there's a set amount of them. For each concrete
+ class implementation of a given Type, we create a XXXfield class that
+ implements HasField
+
+ eg.
+
+ ```java
+ protected abstract static class STArrayField implements HasField{}
+ public static STArrayField starrayField(final Field f) {
+ return new STArrayField(){ @Override public Field getField() {return f;}};
+ }
+ ```
+
+ Then we can create static members on the concrete class
+
+ ```java
+ static public STArrayField AffectedNodes = starrayField(Field.AffectedNodes);
+ static public STArrayField Signatures = starrayField(Field.Signatures);
+ static public STArrayField Template = starrayField(Field.Template);
+ ```
+
+ Later this is used create an api that looks as so
+
+ ```java
+ if (transactionType() == TransactionType.Payment && meta.has(STArray.AffectedNodes)) {
+ STArray affected = meta.get(STArray.AffectedNodes);
+ for (STObject node : affected) {
+ if (node.has(STObject.CreatedNode)) {
+ STObject created = node.get(STObject.CreatedNode);
+ ```
+
+ This is implemented by overloading get()
+
+ ```java
+ public STArray get(STArray.STArrayField f) {
+ return (STArray) fields.get(f.getField());
+ }
+ ```
+
+### com.ripple.core.serialized
+
+#### com.ripple.core.serialized.SerializedType
+
+```java
+public interface SerializedType {
+ Object toJSON();
+ byte[] toBytes();
+ String toHex();
+ void toBytesSink(BytesSink to);
+ Type type();
+}
+```
+
+#### com.ripple.core.serialized.BytesList
+
+A dynamic array of byte[]. Used by TypeTranslators to avoid needless
+copying (see fromParser(parser, hint)).
+
+#### com.ripple.core.serialized.BinaryParser
+
+Responsible for decoding Fields and VL encoded structures.
+
+#### com.ripple.core.serialized.TypeTranslator
+
+Handles converting a SerializedType instances to/from json, binary and other non
+SerializedType values.
+
+Has methods like fromHex, fromBytes, which delegate to fromParser.
+
+#### com.ripple.core.serialized.BinarySerializer
+
+Responsible for encoding Fields/SerializeType into binary.
+
+## Notes
+
+* In the C++ implementation of serialized objects, an STObject can, itself, be
+ assigned a Field, which is stored outside of the associative structure.
+
+ This is problematic when storing as json. Consider this pseudocode.
+
+ ```python
+ >>> so = STObject()
+ >>> so.name = "FieldName"
+ >>> so["FieldOfDreams"] = "A Kevin Costner Movie"
+ >>> sa = STArray([so])
+ ```
+
+ How could `sa` be declared as json?
+
+ ```json
+ >>> [{"FieldName" : {"FieldOfDreams": "A Kevin Costner Movie"}}]
+ ```
+
+ This is in fact how rippled works. There is no 1:1 mapping of STObject to {}
+
+ In ripple-lib(-java)? there is, and single key children of STArrays are enforced.
+
+* Ripple uses 32 byte hashes for object indexes, taking half of a SHA512 hash,
+ which is ~%33 faster than SHA256.
+
+* Simply using google protocol buffers was considered inadequate [link](https://github.com/ripple/rippled/blob/ee51968820fc41c5aeadf2067bfdae54ff21fa66/BinaryFormats.txt#L16)
\ No newline at end of file
diff --git a/ripple-core/build.gradle b/ripple-core/build.gradle
new file mode 100644
index 0000000000..7ee498ed2c
--- /dev/null
+++ b/ripple-core/build.gradle
@@ -0,0 +1,52 @@
+apply plugin: 'java'
+apply plugin: 'kotlin'
+
+description = 'ripple-core'
+
+version = '0.0.1-SNAPSHOT'
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+dependencies {
+ compile 'net.i2p.crypto:eddsa:0.2.0'
+ compile 'org.bouncycastle:bcprov-jdk15on:1.58'
+ compile 'org.json:json:20171018'
+ compile 'com.fasterxml.jackson.core:jackson-databind:2.9.3'
+ testCompile 'junit:junit:4.12'
+ testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+ testCompile "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5"
+}
+buildscript {
+ ext.kotlin_version = '1.3.0'
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+repositories {
+ mavenCentral()
+}
+compileKotlin {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+compileTestKotlin {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+task getDeps(type: Copy) {
+ from configurations.compile
+ into "$rootDir/jars/"
+}
+
+kotlin {
+ experimental {
+ coroutines "enable"
+ }
+}
\ No newline at end of file
diff --git a/ripple-core/src/main/java/README.java b/ripple-core/src/main/java/README.java
new file mode 100644
index 0000000000..0d01f2dfc7
--- /dev/null
+++ b/ripple-core/src/main/java/README.java
@@ -0,0 +1,206 @@
+/**
+ *
+ * # Transaction Meta JSON
+ *
+ * {
+ * "TransactionResult": "tesSUCCESS",
+ *
+ * {@link com.ripple.core.types.known.tx.result.TransactionResult}
+ * {@link com.ripple.core.fields.Field.TransactionResult}
+ * {@link com.ripple.core.coretypes.uint.UInt8}
+ *
+ * "TransactionIndex": 0,
+ *
+ * {@link com.ripple.core.types.known.tx.result.TransactionMeta#transactionIndex}
+ * {@link com.ripple.core.coretypes.uint.UInt32}
+ *
+ * "AffectedNodes": [
+ *
+ * {@link com.ripple.core.types.known.tx.result.TransactionMeta#affectedNodes}
+ *
+ * {
+ * "CreatedNode": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#isCreatedNode()}
+ *
+ * "NewFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "Sequence": 103929,
+ *
+ * {@link com.ripple.core.fields.Field#Sequence}
+ *
+ *
+ * "TakerGets": {
+ *
+ * {@link com.ripple.core.coretypes.Amount}
+ *
+ * "currency": "ILS",
+ *
+ * {@link com.ripple.core.coretypes.Currency}
+ *
+ * "value": "1694.768",
+ *
+ * {@link com.ripple.core.coretypes.AccountID}
+ *
+ * "issuer": "rNPRNzBB92BVpAhhZr4iXDTveCgV5Pofm9"
+ *
+ * {@link java.math.BigDecimal}
+ *
+ * },
+ *
+ *
+ * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
+ *
+ * {@link com.ripple.core.coretypes.AccountID}
+ *
+ * "BookDirectory": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000",
+ *
+ * {@link com.ripple.core.coretypes.Quality#fromBookDirectory}
+ *
+ * "TakerPays": "98957503520",
+ *
+ * {@link com.ripple.core.coretypes.Amount}
+ * {@link com.ripple.core.coretypes.Amount#isNative()}
+ *
+ * "OwnerNode": "000000000000000E"
+ *
+ * {@link com.ripple.core.types.known.sle.entries.Offer#ownerNodeDirectoryIndex}
+ * },
+ *
+ * "LedgerIndex": "3596CE72C902BAFAAB56CC486ACAF9B4AFC67CF7CADBB81A4AA9CBDC8C5CB1AA",
+ *
+ * {@link com.ripple.core.coretypes.hash.Hash256#index}
+ *
+ * "LedgerEntryType": "Offer"
+ *
+ * {@link com.ripple.core.serialized.enums.LedgerEntryType}
+ * {@link com.ripple.core.types.known.sle.entries.Offer}
+ * }
+ * },
+ *
+ *
+ * {
+ * "CreatedNode": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#isCreatedNode()}
+ *
+ * "NewFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "RootIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000",
+ *
+ * com.ripple.core.coretypes.hash.Hash256#index
+ *
+ * "TakerGetsIssuer": "92D705968936C419CE614BF264B5EEB1CEA47FF4",
+ *
+ * {@link com.ripple.core.coretypes.Currency}
+ * {@link com.ripple.core.coretypes.hash.Hash160}
+ * {@link com.ripple.core.fields.Field#TakerGetsIssuer}
+ *
+ * "ExchangeRate": "5C14BE8A20D7F000",
+ *
+ * {@link com.ripple.core.coretypes.Quality#fromBookDirectory}
+ *
+ * "TakerGetsCurrency": "000000000000000000000000494C530000000000"
+ *
+ * {@link com.ripple.core.coretypes.Currency}
+ * {@link com.ripple.core.coretypes.hash.Hash160}
+ * {@link com.ripple.core.fields.Field#TakerGetsCurrency}
+ *
+ * },
+ *
+ * "LedgerIndex": "62A3338CAF2E1BEE510FC33DE1863C56948E962CCE173CA55C14BE8A20D7F000",
+ *
+ * {@link com.ripple.core.coretypes.hash.Hash256#index}
+ *
+ * "LedgerEntryType": "DirectoryNode"
+ *
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode}
+ * {@link com.ripple.core.serialized.enums.LedgerEntryType#DirectoryNode}
+ * }
+ * },
+ *
+ * {
+ * "ModifiedNode": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()}
+ *
+ * "FinalFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "RootIndex": "801C5AFB5862D4666D0DF8E5BE1385DC9B421ED09A4269542A07BC0267584B64",
+ *
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#rootIndex}
+ *
+ * "Flags": 0,
+ * "Owner": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
+ *
+ * {@link com.ripple.core.coretypes.hash.Index#ownerDirectory}
+ *
+ * "IndexPrevious": "0000000000000000"
+ *
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#hasPreviousIndex}
+ * {@link com.ripple.core.types.known.sle.entries.DirectoryNode#prevIndex}
+ * },
+ *
+ * "LedgerIndex": "AB03F8AA02FFA4635E7CE2850416AEC5542910A2B4DBE93C318FEB08375E0DB5",
+ *
+ * "LedgerEntryType": "DirectoryNode"
+ * }
+ * },
+ * {
+ * "ModifiedNode": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode}
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#isModifiedNode()}
+ *
+ * "FinalFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsFinal()}
+ *
+ * "Sequence": 103930,
+ * "Flags": 0,
+ * "Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
+ * "OwnerCount": 9,
+ * "Balance": "106861218302"
+ *
+ * {@link com.ripple.core.coretypes.Amount}
+ * },
+ *
+ * "LedgerIndex": "CF23A37E39A571A0F22EC3E97EB0169936B520C3088963F16C5EE4AC59130B1B",
+ *
+ * {@link com.ripple.core.coretypes.hash.Index#accountRoot}
+ *
+ * "LedgerEntryType": "AccountRoot",
+ *
+ * {@link com.ripple.core.types.known.sle.entries.AccountRoot}
+ * {@link com.ripple.core.serialized.enums.LedgerEntryType#AccountRoot}
+ *
+ * "PreviousFields": {
+ *
+ * {@link com.ripple.core.types.known.tx.result.AffectedNode#nodeAsPrevious()}
+ *
+ * "Sequence": 103929,
+ * "OwnerCount": 8,
+ * "Balance": "106861218312"
+ * },
+ *
+ * "PreviousTxnID": "DE15F43F4A73C4F6CB1C334D9E47BDE84467C0902796BB81D4924885D1C11E6D",
+ *
+ * {@link com.ripple.core.types.known.tx.Transaction#hash}
+ *
+ * "PreviousTxnLgrSeq": 3225338
+ *
+ * {@link com.ripple.core.coretypes.uint.UInt32}
+ * }
+ * }
+ * ]
+ * }
+*/
\ No newline at end of file
diff --git a/ripple-core/src/main/java/com/ripple/core/binary/FileSTWriter.java b/ripple-core/src/main/java/com/ripple/core/binary/FileSTWriter.java
new file mode 100644
index 0000000000..e46af9db43
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/binary/FileSTWriter.java
@@ -0,0 +1,32 @@
+package com.ripple.core.binary;
+
+import com.ripple.core.serialized.StreamSink;
+
+import java.io.*;
+
+public class FileSTWriter extends STWriter implements Closeable {
+ BufferedOutputStream out;
+
+ private FileSTWriter(StreamSink sink, BufferedOutputStream out) {
+ super(sink);
+ this.out = out;
+ }
+
+ public static FileSTWriter fromFile(String path) {
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(path);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ BufferedOutputStream out = new BufferedOutputStream(fos);
+ StreamSink sink = new StreamSink(out);
+
+ return new FileSTWriter(sink, out);
+ }
+
+ @Override
+ public void close() throws IOException {
+ out.close();
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/binary/STReader.java b/ripple-core/src/main/java/com/ripple/core/binary/STReader.java
new file mode 100644
index 0000000000..dc3655073b
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/binary/STReader.java
@@ -0,0 +1,126 @@
+package com.ripple.core.binary;
+
+import com.ripple.core.coretypes.*;
+import com.ripple.core.coretypes.hash.Hash128;
+import com.ripple.core.coretypes.hash.Hash160;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.uint.UInt16;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.coretypes.uint.UInt8;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.StreamBinaryParser;
+import com.ripple.core.types.known.sle.LedgerEntry;
+import com.ripple.core.types.known.tx.Transaction;
+import com.ripple.core.types.known.tx.result.TransactionMeta;
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+import java.util.Arrays;
+import java.util.Date;
+
+public class STReader {
+ protected BinaryParser parser;
+ public STReader(BinaryParser parser) {
+ this.parser = parser;
+ }
+ public STReader(String hex) {
+ this.parser = new BinaryParser(hex);
+ }
+
+ public static STReader fromFile(String arg) {
+ return new STReader(StreamBinaryParser.fromFile(arg));
+ }
+
+ public UInt8 uInt8() {
+ return UInt8.fromParser(parser);
+ }
+ public UInt16 uInt16() {
+ return UInt16.fromParser(parser);
+ }
+ public UInt32 uInt32() {
+ return UInt32.fromParser(parser);
+ }
+ public UInt64 uInt64() {
+ return UInt64.fromParser(parser);
+ }
+ public Hash128 hash128() {
+ return Hash128.fromParser(parser);
+ }
+ public Hash160 hash160() {
+ return Hash160.fromParser(parser);
+ }
+ public Currency currency() {
+ return Currency.fromParser(parser);
+ }
+ public Hash256 hash256() {
+ return Hash256.fromParser(parser);
+ }
+ public Vector256 vector256() {
+ return Vector256.fromParser(parser);
+ }
+ public AccountID accountID() {
+ return AccountID.fromParser(parser);
+ }
+ public Blob variableLength() {
+ int hint = parser.readVLLength();
+ return Blob.fromParser(parser, hint);
+ }
+ public Amount amount() {
+ return Amount.fromParser(parser);
+ }
+ public PathSet pathSet() {
+ return PathSet.fromParser(parser);
+ }
+
+ public STObject stObject() {
+ return STObject.fromParser(parser);
+ }
+ public STObject vlStObject() {
+ return STObject.fromParser(parser, parser.readVLLength());
+ }
+
+ public HashPrefix hashPrefix() {
+ byte[] read = parser.read(4);
+ for (HashPrefix hashPrefix : HashPrefix.values()) {
+ if (Arrays.equals(read, hashPrefix.bytes())) {
+ return hashPrefix;
+ }
+ }
+ return null;
+ }
+
+ public STArray stArray() {
+ return STArray.fromParser(parser);
+ }
+ public Date rippleDate() {
+ return RippleDate.fromParser(parser);
+ }
+
+ public BinaryParser parser() {
+ return parser;
+ }
+
+ public TransactionResult readTransactionResult(UInt32 ledgerIndex) {
+ Hash256 hash = hash256();
+ Transaction txn = (Transaction) vlStObject();
+ TransactionMeta meta = (TransactionMeta) vlStObject();
+ return new TransactionResult(ledgerIndex.longValue(), hash, txn, meta);
+ }
+
+ public LedgerEntry readLE() {
+ Hash256 index = hash256();
+ STObject object = vlStObject();
+ LedgerEntry le = (LedgerEntry) object;
+ le.index(index);
+ return le;
+ }
+
+ public int readOneInt() {
+ return parser.readOneInt();
+ }
+
+ public boolean end() {
+ return parser.end();
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/binary/STWriter.java b/ripple-core/src/main/java/com/ripple/core/binary/STWriter.java
new file mode 100644
index 0000000000..77f28eb7f7
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/binary/STWriter.java
@@ -0,0 +1,71 @@
+package com.ripple.core.binary;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.serialized.BinarySerializer;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.StreamSink;
+import com.ripple.core.types.known.sle.LedgerEntry;
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+import java.io.*;
+
+public class STWriter implements BytesSink, Closeable {
+ BytesSink sink;
+ BinarySerializer serializer;
+ public STWriter(BytesSink bytesSink) {
+ serializer = new BinarySerializer(bytesSink);
+ sink = bytesSink;
+ }
+
+ private OutputStream stream;
+ private STWriter(BytesSink sink, OutputStream stream) {
+ this(sink);
+ this.stream = stream;
+ }
+
+ public static STWriter toFile(String path) {
+ try {
+ FileOutputStream fos = new FileOutputStream(path);
+ BufferedOutputStream bos = new BufferedOutputStream(fos);
+ return new STWriter(new StreamSink(bos), bos);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void write(SerializedType obj) {
+ obj.toBytesSink(sink);
+ }
+ public void writeVl(SerializedType obj) {
+ serializer.addLengthEncoded(obj);
+ }
+
+ @Override
+ public void add(byte aByte) {
+ sink.add(aByte);
+ }
+
+ @Override
+ public void add(byte[] bytes) {
+ sink.add(bytes);
+ }
+
+ public void write(TransactionResult result) {
+ write(result.hash);
+ writeVl(result.txn);
+ writeVl(result.meta);
+ }
+
+ public void write(Hash256 hash256, LedgerEntry le) {
+ write(hash256);
+ writeVl(le);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (this.stream != null) {
+ this.stream.close();
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/cache/SLECache.java b/ripple-core/src/main/java/com/ripple/core/cache/SLECache.java
new file mode 100644
index 0000000000..55552ede01
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/cache/SLECache.java
@@ -0,0 +1,110 @@
+package com.ripple.core.cache;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.types.known.sle.LedgerEntry;
+import com.ripple.core.types.known.tx.result.AffectedNode;
+import com.ripple.core.types.known.tx.result.TransactionMeta;
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+import java.util.TreeMap;
+
+public class SLECache {
+ private final TreeMap cache = new TreeMap();
+
+ public static class CacheEntry {
+ public LedgerEntry le;
+ public UInt32 prevTxnIndex;
+ public UInt32 prevLedger;
+ public boolean deleted = false;
+
+ public void upateLedgerEntry(LedgerEntry le, UInt32 ledgerIndex, UInt32 txnIndex) {
+ if (doUpdate(txnIndex, ledgerIndex)) {
+ // in the first case
+ prevTxnIndex = txnIndex;
+ prevLedger = ledgerIndex;
+
+ if (le == null) {
+ deleted = true;
+ }
+ this.le = le;
+ }
+ }
+
+ private boolean doUpdate(UInt32 txnIndex, UInt32 ledgerIndex) {
+ if (le == null && !deleted) {
+ return true;
+ }
+ if (prevLedger == null) {
+ return true;
+ }
+ int ledgerCmp = ledgerIndex.compareTo(prevLedger);
+ if (ledgerCmp == 1) {
+ return true;
+ }
+ if (ledgerCmp == 0) {
+ if (prevTxnIndex == null) {
+ // We don't know, should log a warning or something
+ // Should we keep the first one that we have of this index
+ // or can we assume that the latest is the best?
+ return true;
+ }
+ if (txnIndex.compareTo(prevTxnIndex) == 1) {
+ // This happened AFTER
+ return true;
+ }
+ }
+ //ledgerCmp == -1 or txnIndex <= previousTxnIndex ss
+ return false;
+ }
+ }
+
+ public boolean cache(LedgerEntry le, UInt32 validatedLedgerIndex) {
+ Hash256 index = le.ledgerIndex();
+ CacheEntry ce = getOrCreate(index);
+ ce.upateLedgerEntry(le, validatedLedgerIndex, null);
+ return true;
+ }
+
+ private CacheEntry getEntry(Hash256 index) {
+ return cache.get(index);
+ }
+
+ private CacheEntry createEntry(Hash256 index) {
+ CacheEntry ce = new CacheEntry();
+ cache.put(index, ce);
+ return ce;
+ }
+
+ public LedgerEntry get(Hash256 index) {
+ CacheEntry entry = getEntry(index);
+ return entry == null || entry.deleted ? null : entry.le;
+ }
+
+ public void updateFromTransactionResult(TransactionResult tr) {
+ if (!tr.validated) {
+ return;
+ }
+
+ TransactionMeta meta = tr.meta;
+ UInt32 ledgerIndex = tr.ledgerIndex;
+ UInt32 txnIndex = meta.transactionIndex();
+
+ for (AffectedNode an : meta.affectedNodes()) {
+ Hash256 index = an.ledgerIndex();
+ CacheEntry ce = getOrCreate(index);
+ ce.upateLedgerEntry(an.isDeletedNode() ? null : (LedgerEntry) an.nodeAsFinal(),
+ ledgerIndex,
+ txnIndex);
+ }
+ }
+
+ private CacheEntry getOrCreate(Hash256 index) {
+ CacheEntry already = getEntry(index);
+ if (already == null) {
+ return createEntry(index);
+ } else {
+ return already;
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/AccountID.java b/ripple-core/src/main/java/com/ripple/core/coretypes/AccountID.java
new file mode 100644
index 0000000000..606c5b2b71
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/AccountID.java
@@ -0,0 +1,188 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.coretypes.hash.Hash160;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.AccountIDField;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.crypto.Seed;
+import com.ripple.crypto.keys.IKeyPair;
+import com.ripple.encodings.addresses.Addresses;
+import com.ripple.encodings.common.B16;
+import com.ripple.utils.Utils;
+
+/**
+ * Originally it was intended that AccountIDs would be variable length so that's
+ * why they are variable length encoded as top level field objects.
+ *
+ * Note however, that in practice, all account ids are just 160 bit hashes.
+ * Consider the fields TakerPaysIssuer and fixed length encoding of issuers in
+ * amount serializations.
+ *
+ * Thus, we extend Hash160 which affords us some functionality.
+ */
+public class AccountID extends Hash160 {
+ public static final AccountID NEUTRAL = fromInteger(1);
+ public static final AccountID XRP_ISSUER = fromInteger(0);
+
+ public final String address;
+
+ public AccountID(byte[] bytes) {
+ this(bytes, encodeAddress(bytes));
+ }
+
+ public AccountID(byte[] bytes, String address) {
+ super(bytes);
+ this.address = address;
+ }
+
+ // Static from* constructors
+ public static AccountID fromString(String value) {
+ if (value.length() == 160 / 4) {
+ return fromBytes(B16.decode(value));
+ } else {
+ return fromAddress(value);
+ }
+ }
+
+ static public AccountID fromAddress(String address) {
+ byte[] bytes = Addresses.decodeAccountID(address);
+ return new AccountID(bytes, address);
+ }
+
+ static public AccountID fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ static public AccountID fromHex(String hex) {
+ return translate.fromHex(hex);
+ }
+
+ public static AccountID fromKeyPair(IKeyPair kp) {
+ byte[] bytes = kp.id();
+ return new AccountID(bytes, encodeAddress(bytes));
+ }
+
+ public static AccountID fromPassPhrase(String phrase) {
+ return fromKeyPair(Seed.fromPassPhrase(phrase).keyPair());
+ }
+
+ static public AccountID fromSeed(String seed) {
+ return fromKeyPair(Seed.getKeyPair(seed));
+ }
+
+ private static AccountID fromInteger(Integer n) {
+ return fromBytes(Utils.padTo160(new UInt32(n).toByteArray()));
+ }
+
+ public static AccountID fromBytes(byte[] bytes) {
+ return new AccountID(bytes, encodeAddress(bytes));
+ }
+
+ @Override
+ public int hashCode() {
+ return address.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return address;
+ }
+
+ public Issue issue(String code) {
+ return new Issue(Currency.fromString(code), this);
+ }
+
+ public Issue issue(Currency c) {
+ return new Issue(c, this);
+ }
+
+ public boolean isNativeIssuer() {
+ return this == XRP_ISSUER || equals(XRP_ISSUER);
+ }
+
+ // SerializedType interface implementation
+ @Override
+ public Object toJSON() {
+ return toString();
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ to.add(bytes());
+ }
+
+ @Override
+ public Type type() {
+ return Type.AccountID;
+ }
+
+ public Hash256 lineIndex(Issue issue) {
+ if (issue.isNative()) throw new AssertionError();
+ return Index.rippleState(this, issue.issuer(), issue.currency());
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public AccountID fromParser(BinaryParser parser, Integer hint) {
+ if (hint == null) {
+ hint = 20;
+ }
+ return AccountID.fromBytes(parser.read(hint));
+ }
+
+ @Override
+ public String toString(AccountID obj) {
+ return obj.toString();
+ }
+
+ @Override
+ public AccountID fromString(String value) {
+ return AccountID.fromString(value);
+ }
+ }
+
+ //
+
+ static public Translator translate = new Translator();
+
+ // helpers
+
+ private static String encodeAddress(byte[] address) {
+ return Addresses.encodeAccountID(address);
+ }
+
+ // Typed field definitions
+ private static AccountIDField accountField(final Field f) {
+ return new AccountIDField() {
+ @Override
+ public Field getField() {
+ return f;
+ }
+ };
+ }
+
+ static public AccountIDField Account = accountField(Field.Account);
+ static public AccountIDField Owner = accountField(Field.Owner);
+ static public AccountIDField Destination = accountField(Field.Destination);
+ static public AccountIDField Issuer = accountField(Field.Issuer);
+ static public AccountIDField Target = accountField(Field.Target);
+ static public AccountIDField RegularKey = accountField(Field.RegularKey);
+ static public AccountIDField Authorize = accountField(Field.Authorize);
+ static public AccountIDField Unauthorize = accountField(Field.Unauthorize);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Amount.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Amount.java
new file mode 100644
index 0000000000..3813a5ac09
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Amount.java
@@ -0,0 +1,790 @@
+package com.ripple.core.coretypes;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.fields.AmountField;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import org.json.JSONObject;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.Objects;
+
+/**
+ * In ripple, amounts are either XRP, the native currency, or an IOU of
+ * a given currency as issued by a designated account.
+ */
+public class Amount extends Number implements SerializedType, Comparable
+
+{
+
+ private static BigDecimal TAKER_PAYS_FOR_THAT_DAMN_OFFER = new BigDecimal("1000000000000.000100");
+
+// public static final Amount NEUTRAL_ZERO = new Amount(Currency.NEUTRAL, AccountID.NEUTRAL);
+
+ /**
+ * Thrown when an Amount is constructed with an invalid value
+ */
+ public static class PrecisionError extends RuntimeException {
+ public Amount illegal;
+
+ public PrecisionError(String s) {
+ super(s);
+ }
+
+ public PrecisionError(String s, Amount amount) {
+ super(s);
+ illegal = amount;
+ }
+ }
+
+ // For rounding/multiplying/dividing
+ public static final MathContext MATH_CONTEXT = new MathContext(16, RoundingMode.HALF_UP);
+ // The maximum amount of digits in mantissa of an IOU amount
+ public static final int MAXIMUM_IOU_PRECISION = 16;
+ // The smallest quantity of an XRP is a drop, 1 millionth of an XRP
+ public static final int MAXIMUM_NATIVE_SCALE = 6;
+ // Defines bounds for native amounts
+ public static final BigDecimal MAX_NATIVE_VALUE = parseDecimal("100,000,000,000.0");
+ public static final BigDecimal MIN_NATIVE_VALUE = parseDecimal("0.000,001");
+
+ // These are flags used when serializing to binary form
+ public static final UInt64 BINARY_FLAG_IS_IOU = new UInt64("8000000000000000", 16);
+ public static final UInt64 BINARY_FLAG_IS_NON_NEGATIVE_NATIVE = new UInt64("4000000000000000", 16);
+
+ public static final Amount ONE_XRP = fromString("1.0");
+
+ // TODO, even though all these fields are effectively final, and only
+ // ever set it would be nice to actually use the final modifier.
+ // Perhaps create an amount builder.
+
+ // The quantity of XRP or Issue(currency/issuer pairing)
+ // When native, the value unit is XRP, not drops.
+ private BigDecimal value;
+ private Currency currency;
+ // If the currency is XRP
+ private boolean isNative;
+ // Normally, in the constructor of an Amount the value is checked
+ // that it's scale/precision and quantity are correctly bounded.
+ // If unbounded is true, these checks are skipped.
+ // This is there for historical ledgers that contain amounts that
+ // would now be considered malformed (in the sense of the transaction
+ // engine result class temMALFORMED)
+ private boolean unbounded = false;
+ // The ZERO account is used for specifying the issuer for native
+ // amounts. In practice the issuer is never used when an
+ // amount is native.
+ private AccountID issuer;
+
+ private UInt64 mantissa;
+ // The exponent is always calculated.
+ private int exponent;
+
+ public Amount(BigDecimal value, Currency currency, AccountID issuer) {
+ this(value, currency, issuer, false);
+ }
+
+ public Amount(BigDecimal xrpScaleValue) {
+ isNative = true;
+ currency = Currency.XRP;
+ this.setAndCheckValue(xrpScaleValue);
+ }
+
+ public Amount(Number xrpScaleValue) {
+ isNative = true;
+ currency = Currency.XRP;
+ this.setAndCheckValue(BigDecimal.valueOf(xrpScaleValue.doubleValue()));
+ }
+
+ public Amount(BigDecimal value, Currency currency, AccountID issuer, boolean isNative, boolean unbounded) {
+ this.isNative = isNative;
+ this.currency = currency;
+ this.unbounded = unbounded;
+ this.setAndCheckValue(value);
+ // done AFTER set value which sets some default values
+ this.issuer = issuer;
+ }
+
+ public Amount(Currency currency, AccountID account) {
+ this(BigDecimal.ZERO, currency, account);
+ }
+
+ // Private constructors
+ Amount(BigDecimal newValue, Currency currency, AccountID issuer, boolean isNative) {
+ this(newValue, currency, issuer, isNative, false);
+ }
+
+ private Amount(BigDecimal value, String currency, String issuer) {
+ this(value, currency);
+ if (issuer != null) {
+ this.issuer = AccountID.fromString(issuer);
+ }
+ }
+
+ public Amount(BigDecimal value, String currency) {
+ isNative = false;
+ this.currency = Currency.fromString(currency);
+ this.setAndCheckValue(value);
+ }
+
+ private void setAndCheckValue(BigDecimal value) {
+ this.value = value.stripTrailingZeros();
+ initialize();
+ }
+
+ private void initialize() {
+ if (isNative()) {
+ issuer = AccountID.XRP_ISSUER;
+ if (!unbounded) {
+ checkXRPBounds();
+ }
+ // Offset is unused for native amounts
+ exponent = -6; // compared to drops.
+ } else {
+ issuer = AccountID.NEUTRAL;
+ exponent = calculateExponent();
+
+ if (value.precision() > MAXIMUM_IOU_PRECISION && !unbounded) {
+ String err = "value precision of " + value.precision() +
+ " is greater than maximum " +
+ "iou precision of " + MAXIMUM_IOU_PRECISION;
+ throw new PrecisionError(err, this);
+ }
+ }
+ mantissa = calculateMantissa();
+ }
+
+ private Amount newValue(BigDecimal newValue) {
+ return newValue(newValue, false, false);
+ }
+
+ private Amount newValue(BigDecimal newValue, boolean round, boolean unbounded) {
+ if (round) {
+ newValue = roundValue(newValue, isNative);
+ }
+ return new Amount(newValue, currency, issuer, isNative, unbounded);
+ }
+
+ private Amount newValue(BigDecimal val, boolean round) {
+ return newValue(val, round, false);
+ }
+
+ /* Getters and Setters */
+
+ public BigDecimal value() {
+ return value;
+ }
+
+ public Currency currency() {
+ return currency;
+ }
+
+ public AccountID issuer() {
+ return issuer;
+ }
+
+ public Issue issue() {
+ // TODO: store the currency and issuer as an Issue
+ return new Issue(currency, issuer);
+ }
+
+ private UInt64 mantissa() {
+ // Having this lazily computed and then cached would give significant
+ // performance boosts in some single threaded contexts. Given the inputs
+ // for the method are at least effectively final, and the method is
+ // essentially a pure function, it seems probably safe to do. It
+ // seems at worst, that in a multi threaded context, some extra work
+ // will be done in rare cases?
+ // TODO: check this assumption
+ return mantissa;
+ }
+
+ public int exponent() {
+ return exponent;
+ }
+
+ public boolean isNative() {
+ return isNative;
+ }
+
+ public String currencyString() {
+ return currency.toString();
+ }
+
+ public String issuerString() {
+ if (issuer == null) {
+ return "";
+ }
+ return issuer.toString();
+ }
+
+ /* Offset & Mantissa Helpers */
+
+ /**
+ * @return a positive value for the mantissa
+ */
+ private UInt64 calculateMantissa() {
+ if (isNative()) {
+ return new UInt64(bigIntegerDrops().abs());
+ } else {
+ return new UInt64(bigIntegerIOUMantissa());
+ }
+ }
+
+ private int calculateExponent() {
+ return -MAXIMUM_IOU_PRECISION + value.precision() - value.scale();
+ }
+
+ private BigInteger bigIntegerIOUMantissa() {
+ return exactBigIntegerScaledByPowerOfTen(-exponent).abs();
+ }
+
+ private BigInteger bigIntegerDrops() {
+ return exactBigIntegerScaledByPowerOfTen(MAXIMUM_NATIVE_SCALE);
+ }
+
+ private BigInteger exactBigIntegerScaledByPowerOfTen(int n) {
+ return value.scaleByPowerOfTen(n).toBigIntegerExact();
+ }
+
+ /* Equality testing */
+
+ private boolean equalValue(Amount amt) {
+ return compareTo(amt) == 0;
+ }
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Amount) {
+ return equals((Amount) obj);
+ }
+ return super.equals(obj);
+ }
+
+ public boolean equals(Amount amt) {
+ return equalValue(amt) &&
+ currency.equals(amt.currency) &&
+ (isNative() || issuer.equals(amt.issuer));
+ }
+
+ public boolean equalsExceptIssuer(Amount amt) {
+ return equalValue(amt) &&
+ currencyString().equals(amt.currencyString());
+ }
+
+ public int compareTo(Amount amount) {
+ Objects.requireNonNull(amount);
+ return value.compareTo(amount.value);
+ }
+
+ public boolean isZero() {
+ return value.signum() == 0;
+ }
+
+ public boolean isNegative() {
+ return value.signum() == -1;
+ }
+
+ // Maybe you want !isNegative()
+ // Any amount that !isNegative() isn't necessarily positive
+ // Is a zero amount strictly positive? no
+ public boolean isPositive() {
+ return value.signum() == 1;
+ }
+
+ /**
+
+ Arithmetic Operations
+
+ There's no checking if an amount is of a different currency/issuer.
+
+ All operations return amounts of the same currency/issuer as the
+ first operand.
+
+ eg.
+
+ amountOne.add(amountTwo)
+
+ The currency/issuer of the resultant amount, is that of `amountOne`
+
+ Divide and multiply are equivalent to the javascript ripple-lib
+ ratio_human and product_human.
+
+ */
+ public Amount add(BigDecimal augend) {
+ return newValue(value.add(augend), true);
+ }
+
+ public Amount add(Amount augend) {
+ return add(augend.value);
+ }
+
+ public Amount add(Number augend) {
+ return add(BigDecimal.valueOf(augend.doubleValue()));
+ }
+
+ public Amount subtract(BigDecimal subtrahend) {
+ return newValue(value.subtract(subtrahend), true);
+ }
+
+ public Amount subtract(Amount subtrahend) {
+ return subtract(subtrahend.value);
+ }
+
+ public Amount subtract(Number subtrahend) {
+ return subtract(BigDecimal.valueOf(subtrahend.doubleValue()));
+ }
+
+ public Amount multiply(BigDecimal divisor) {
+ return newValue(value.multiply(divisor, MATH_CONTEXT), true);
+ }
+
+ public Amount multiply(Amount multiplicand) {
+ return multiply(multiplicand.value);
+ }
+
+ public Amount multiply(Number multiplicand) {
+ return multiply(BigDecimal.valueOf(multiplicand.doubleValue()));
+ }
+
+ public Amount divide(BigDecimal divisor) {
+ return newValue(value.divide(divisor, MATH_CONTEXT), true);
+ }
+
+ public Amount divide(Amount divisor) {
+ return divide(divisor.value);
+ }
+
+ public Amount divide(Number divisor) {
+ return divide(BigDecimal.valueOf(divisor.doubleValue()));
+ }
+
+ public Amount negate() {
+ return newValue(value.negate());
+ }
+
+ public Amount abs() {
+ return newValue(value.abs());
+ }
+ public Amount min(Amount val) {
+ return (compareTo(val) <= 0 ? this : val);
+ }
+ public Amount max(Amount val) {
+ return (compareTo(val) >= 0 ? this : val);
+ }
+
+ /* Offer related helpers */
+ public BigDecimal computeQuality(Amount toExchangeThisWith) {
+ return value.divide(toExchangeThisWith.value, MathContext.DECIMAL128);
+ }
+ /**
+ * @return Amount
+ * The real native unit is a drop, one million of which are an XRP.
+ * We want `one` unit at XRP scale (1e6 drops), or if it's an IOU,
+ * just `one`.
+ */
+ public Amount one() {
+ if (isNative()) {
+ return ONE_XRP;
+ } else {
+ return issue().amount(1);
+ }
+ }
+
+ /* Serialized Type implementation */
+
+ @Override
+ public Object toJSON() {
+ if (isNative()) {
+ return toDropsString();
+ } else {
+ return toJSONObject();
+ }
+ }
+
+ public JSONObject toJSONObject() {
+ if (isNative()) {
+ throw new RuntimeException("Native amounts must be serialized as a string");
+ }
+
+ JSONObject out = new JSONObject();
+ out.put("currency", currencyString());
+ out.put("value", valueText());
+ out.put("issuer", issuerString());
+ return out;
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ // TODO: probably better off using long
+ UInt64 man = mantissa();
+
+ if (isNative()) {
+ if (!isNegative()) {
+ man = man.or(BINARY_FLAG_IS_NON_NEGATIVE_NATIVE);
+ }
+ to.add(man.toByteArray());
+ } else {
+ int exponent = exponent();
+ UInt64 packed;
+
+ if (isZero()) {
+ packed = BINARY_FLAG_IS_IOU;
+ } else if (isNegative()) {
+ packed = man.or(new UInt64(512 + /* 0 + */ 97 + exponent).shiftLeft(64 - 10));
+ } else {
+ packed = man.or(new UInt64(512 + 256 + 97 + exponent).shiftLeft(64 - 10));
+ }
+
+ to.add(packed.toByteArray());
+ to.add(currency.bytes());
+ to.add(issuer.bytes());
+ }
+ }
+
+ @Override
+ public Type type() {
+ return Type.Amount;
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public Amount fromString(String s) {
+ // We need to use the full dotted.path here, otherwise
+ // we get confused with the AmountField Amount
+ return com.ripple.core.coretypes.Amount.fromString(s);
+ }
+
+ @Override
+ public Amount fromParser(BinaryParser parser, Integer hint) {
+ BigDecimal value;
+ byte[] mantissa = parser.read(8);
+ byte b1 = mantissa[0], b2 = mantissa[1];
+
+ boolean isIOU = (b1 & 0x80) != 0;
+ boolean isPositive = (b1 & 0x40) != 0;
+ int sign = isPositive ? 1 : -1;
+
+ if (isIOU) {
+ mantissa[0] = 0;
+ Currency curr = Currency.fromParser(parser);
+ AccountID issuer = AccountID.fromParser(parser);
+ int exponent = ((b1 & 0x3F) << 2) + ((b2 & 0xff) >> 6) - 97;
+ mantissa[1] &= 0x3F;
+
+ value = new BigDecimal(new BigInteger(sign, mantissa), -exponent);
+ return new Amount(value, curr, issuer, false);
+ } else {
+ mantissa[0] &= 0x3F;
+ value = xrpFromDropsMantissa(mantissa, sign);
+ return new Amount(value);
+ }
+ }
+
+ @Override
+ public String toString(Amount obj) {
+ return obj.stringRepr();
+ }
+
+ public JSONObject toJSONObject(Amount obj) {
+ return obj.toJSONObject();
+ }
+
+ @Override
+ public Amount fromJSONObject(JSONObject jsonObject) {
+ String valueString = jsonObject.getString("value");
+ String issuerString = jsonObject.getString("issuer");
+ String currencyString = jsonObject.getString("currency");
+ return new Amount(new BigDecimal(valueString), currencyString, issuerString);
+ }
+
+ @Override
+ public Amount fromJacksonObject(ObjectNode object) {
+ checkField(object, "value");
+ checkField(object, "issuer");
+ checkField(object, "currency");
+ String value = object.get("value").asText();
+ String issuer = object.get("issuer").asText();
+ String currency = object.get("currency").asText();
+ return new Amount(new BigDecimal(value), currency, issuer);
+ }
+
+ private void checkField(ObjectNode object, String field) {
+ if (!object.has(field)) {
+ throw new IllegalArgumentException(object + "is missing `" +
+ field + "`");
+ }
+ }
+ }
+ static public Translator translate = new Translator();
+
+ public static BigDecimal xrpFromDropsMantissa(byte[] mantissa, int sign) {
+ return new BigDecimal(new BigInteger(sign, mantissa), 6);
+ }
+
+ /* Number overides */
+ @Override
+ public int intValue() {
+ return value.intValueExact();
+ }
+
+ @Override
+ public long longValue() {
+ return value.longValueExact();
+ }
+
+ @Override
+ public float floatValue() {
+ return value.floatValue();
+ }
+
+ @Override
+ public double doubleValue() {
+ return value.doubleValue();
+ }
+
+ public BigInteger bigIntegerValue() {
+ return value.toBigIntegerExact();
+ }
+
+ public Amount newIssuer(AccountID issuer) {
+ return new Amount(value, currency, issuer, isNative, unbounded);
+ }
+
+ public Amount copy() {
+ return new Amount(value, currency, issuer, isNative, unbounded);
+ }
+
+ // Static constructors
+ public static Amount fromString(String val) {
+ if (val.contains("/")) {
+ return fromIOUString(val);
+ } else if (val.contains(".")) {
+ return fromXrpString(val);
+ } else {
+ return fromDropString(val);
+ }
+
+ }
+
+ public static Amount fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ public static Amount fromHex(String hex) {
+ return translate.fromHex(hex);
+ }
+ public static Amount fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ public static Amount fromJSONObject(JSONObject jsonObject) {
+ return translate.fromJSONObject(jsonObject);
+ }
+
+ public static Amount fromJacksonObject(ObjectNode object) {
+ return translate.fromJacksonObject(object);
+ }
+
+ public static Amount fromValue(Object amount) {
+ return translate.fromValue(amount);
+ }
+
+ public static Amount fromDropString(String val) {
+ BigDecimal xrp = new BigDecimal(val).scaleByPowerOfTen(-6);
+ checkDropsValueWhole(val);
+ return new Amount(xrp);
+ }
+
+ public static Amount fromIOUString(String val) {
+ String[] split = val.split("/");
+ if (split.length == 1) {
+ throw new RuntimeException("IOU string must be in the form " +
+ "number/currencyString or " +
+ "number/currencyString/issuerString");
+ } else if (split.length == 2) {
+ return new Amount(new BigDecimal(split[0]), split[1]);
+ } else {
+ return new Amount(new BigDecimal(split[0]), split[1], split[2]);
+ }
+ }
+
+ @Deprecated
+ private static Amount fromXrpString(String valueString) {
+ BigDecimal val = new BigDecimal(valueString);
+ return new Amount(val);
+ }
+
+ /**
+ * @return A String representation as used by ripple json format
+ */
+ public String stringRepr() {
+ if (isNative()) {
+ return toDropsString();
+ } else {
+ return iouTextFull();
+ }
+ }
+
+ public String toDropsString() {
+ if (!isNative()) {
+ throw new RuntimeException("Amount is not native");
+ }
+ return bigIntegerDrops().toString();
+ }
+
+ private String iouText() {
+ return String.format("%s/%s", valueText(), currencyString());
+ }
+
+ public String iouTextFull() {
+ return String.format("%s/%s/%s", valueText(), currencyString(), issuerString());
+ }
+
+ public String toTextFull() {
+ if (isNative()) {
+ return nativeText();
+ } else {
+ return iouTextFull();
+ }
+ }
+
+ public String nativeText() {
+ return String.format("%s/XRP", valueText());
+ }
+
+ @Override
+ public String toString() {
+ return toTextFull();
+ }
+
+ public String toText() {
+ if (isNative()) {
+ return nativeText();
+ } else {
+ return iouText();
+ }
+ }
+
+ /**
+ * @return A String containing the value (in XRP scale when native)
+ * as a decimal number
+ *
+ */
+ public String valueText() {
+ return value.signum() == 0 ? "0" : value().toPlainString();
+ }
+
+ private void checkLowerDropBound(BigDecimal val) {
+ if (val.scale() > 6) {
+ PrecisionError bigger = getOutOfBoundsError(val,
+ "smaller than min native value",
+ MIN_NATIVE_VALUE);
+ bigger.illegal = this;
+ throw bigger;
+ }
+ }
+
+ private void checkUpperBound(BigDecimal val) {
+ if (val.compareTo(MAX_NATIVE_VALUE) > 0) {
+ PrecisionError bigger = getOutOfBoundsError(val,
+ "bigger than max native value ",
+ MAX_NATIVE_VALUE);
+ bigger.illegal = this;
+ throw bigger;
+ }
+ }
+
+ private static PrecisionError getOutOfBoundsError(
+ BigDecimal abs,
+ String sized,
+ BigDecimal bound) {
+ return new PrecisionError(
+ abs.toPlainString() + " absolute XRP is " + sized + bound);
+ }
+
+ private void checkXRPBounds() {
+ BigDecimal v = value.abs();
+ try {
+ checkLowerDropBound(v);
+ checkUpperBound(v);
+ } catch (PrecisionError e) {
+ if (v.compareTo(TAKER_PAYS_FOR_THAT_DAMN_OFFER) == 0) {
+ return;
+ }
+ throw e;
+ }
+ }
+
+
+ private static int significantDigits(BigDecimal input) {
+ input = input.stripTrailingZeros();
+ return input.scale() < 0
+ ? input.precision() - input.scale()
+ : input.precision();
+ }
+
+ public int significantDigits() {
+ return significantDigits(value);
+ }
+
+ public static void checkDropsValueWhole(String drops) {
+ boolean contains = drops.contains(".");
+ if (contains) {
+ throw new RuntimeException("Drops string contains floating point is decimal");
+ }
+ }
+
+ public static BigDecimal roundValue(BigDecimal value, boolean nativeSrc) {
+ int i = value.precision() - value.scale();
+ return value.setScale(nativeSrc ? MAXIMUM_NATIVE_SCALE :
+ MAXIMUM_IOU_PRECISION - i,
+ MATH_CONTEXT.getRoundingMode());
+ }
+
+ private static BigDecimal parseDecimal(String s) {
+ return new BigDecimal(s.replace(",", "")); //# .scaleByPowerOfTen(6);
+ }
+
+ private static AmountField amountField(final Field f) {
+ return new AmountField() {
+ @Override
+ public Field getField() {
+ return f;
+ }
+ };
+ }
+
+ static public AmountField Amount = amountField(Field.Amount);
+ static public AmountField Balance = amountField(Field.Balance);
+ static public AmountField LimitAmount = amountField(Field.LimitAmount);
+ static public AmountField DeliveredAmount = amountField(Field.DeliveredAmount);
+ static public AmountField TakerPays = amountField(Field.TakerPays);
+ static public AmountField TakerGets = amountField(Field.TakerGets);
+ static public AmountField LowLimit = amountField(Field.LowLimit);
+ static public AmountField HighLimit = amountField(Field.HighLimit);
+ static public AmountField Fee = amountField(Field.Fee);
+ static public AmountField SendMax = amountField(Field.SendMax);
+ static public AmountField DeliverMin = amountField(Field.DeliverMin);
+ static public AmountField MinimumOffer = amountField(Field.MinimumOffer);
+ static public AmountField RippleEscrow = amountField(Field.RippleEscrow);
+ static public AmountField taker_gets_funded = amountField(Field.taker_gets_funded);
+ static public AmountField taker_pays_funded = amountField(Field.taker_pays_funded);
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Blob.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Blob.java
new file mode 100644
index 0000000000..48708f78f5
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Blob.java
@@ -0,0 +1,119 @@
+
+package com.ripple.core.coretypes;
+
+import com.ripple.core.fields.BlobField;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+import org.bouncycastle.util.encoders.Hex;
+
+public class Blob implements SerializedType {
+ public Blob(byte[] bytes) {
+ buffer = bytes.clone();
+ }
+
+ private final byte[] buffer;
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return buffer;
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.Blob;
+ }
+
+ public static Blob fromBytes(byte[] bytes) {
+ return new Blob(bytes);
+ }
+
+ public static Blob fromHex(String hex) {
+ return fromBytes(B16.decode(hex));
+ }
+
+ public static Blob fromParser(BinaryParser parser, int hint) {
+ return translate.fromParser(parser, hint);
+ }
+ public static Blob fromParser(BinaryParser parser) {
+ return translate.fromParser(parser, null);
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public Blob fromParser(BinaryParser parser, Integer hint) {
+ if (hint == null) {
+ hint = parser.size() - parser.pos();
+ }
+ return new Blob(parser.read(hint));
+ }
+
+ @Override
+ public Object toJSON(Blob obj) {
+ return toString(obj);
+ }
+
+ @Override
+ public String toString(Blob obj) {
+ return B16.encode(obj.buffer);
+ }
+
+ @Override
+ public Blob fromString(String value) {
+ return new Blob(Hex.decode(value));
+ }
+
+ @Override
+ public void toBytesSink(Blob obj, BytesSink to) {
+ to.add(obj.buffer.clone());
+ }
+ }
+
+ static public Translator translate = new Translator();
+
+ private static BlobField blobField(final Field f) {
+ return new BlobField() {
+ @Override
+ public Field getField() {
+ return f;
+ }
+ };
+ }
+
+ static public BlobField PublicKey = blobField(Field.PublicKey);
+ static public BlobField MessageKey = blobField(Field.MessageKey);
+ static public BlobField SigningPubKey = blobField(Field.SigningPubKey);
+ static public BlobField TxnSignature = blobField(Field.TxnSignature);
+ static public BlobField MasterSignature = blobField(Field.MasterSignature);
+ static public BlobField Signature = blobField(Field.Signature);
+ static public BlobField Domain = blobField(Field.Domain);
+ static public BlobField FundCode = blobField(Field.FundCode);
+ static public BlobField RemoveCode = blobField(Field.RemoveCode);
+ static public BlobField ExpireCode = blobField(Field.ExpireCode);
+ static public BlobField CreateCode = blobField(Field.CreateCode);
+
+ static public BlobField MemoType = blobField(Field.MemoType);
+ static public BlobField MemoData = blobField(Field.MemoData);
+ static public BlobField MemoFormat = blobField(Field.MemoFormat);
+ static public BlobField Condition = blobField(Field.Condition);
+ static public BlobField Fulfillment = blobField(Field.Fulfillment);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Currency.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Currency.java
new file mode 100644
index 0000000000..27832e335b
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Currency.java
@@ -0,0 +1,250 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.coretypes.hash.Hash160;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.encodings.common.B16;
+import com.ripple.utils.Utils;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Funnily enough, yes, in rippled a currency is represented by a Hash160 type.
+ * For the sake of consistency and convenience, this quirk is repeated here.
+ *
+ * https://gist.github.com/justmoon/8597643
+ */
+public class Currency extends Hash160 {
+ public static final Currency NEUTRAL = new Currency(Utils.padTo160(BigInteger.ONE.toByteArray()));
+ public static final Currency XRP = new Currency(Utils.padTo160(BigInteger.ZERO.toByteArray()));
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ public boolean isNative() {
+ return this == Currency.XRP || equals(Currency.XRP);
+ }
+
+ public boolean isIOU() {
+ return !isNative();
+ }
+
+ public static enum Type {
+ HASH,
+ ISO, // three letter isoCode
+ DEMURRAGE,
+ UNKNOWN;
+
+ public static Type fromByte(byte typeByte) {
+ if (typeByte == 0x00) {
+ return ISO;
+ } else if (typeByte == 0x01) {
+ return DEMURRAGE;
+ } else if ((typeByte & 0x80) != 0) {
+ return HASH;
+ } else {
+ return UNKNOWN;
+ }
+ }
+ }
+ Type type;
+
+ public static class Demurrage {
+ Date interestStart;
+ String isoCode;
+ double interestRate;
+
+ static public BigDecimal applyRate(BigDecimal amount, BigDecimal rate, TimeUnit time, long units) {
+ BigDecimal appliedRate = getSeconds(time, units).divide(rate, MathContext.DECIMAL64);
+ BigDecimal factor = BigDecimal.valueOf(Math.exp(appliedRate.doubleValue()));
+ return amount.multiply(factor, MathContext.DECIMAL64);
+ }
+
+ static public BigDecimal calculateRate(BigDecimal rate, TimeUnit time, long units) {
+ BigDecimal seconds = getSeconds(time, units);
+ BigDecimal log = ln(rate);
+ return seconds.divide(log, MathContext.DECIMAL64);
+ }
+
+ private static BigDecimal ln(BigDecimal bd) {
+ return BigDecimal.valueOf(Math.log(bd.doubleValue()));
+ }
+
+ private static BigDecimal getSeconds(TimeUnit time, long units) {
+ return BigDecimal.valueOf(time.toSeconds(units));
+ }
+
+ public Demurrage(byte[] bytes) {
+ BinaryParser parser = new BinaryParser(bytes);
+ parser.skip(1); // The type
+ isoCode = isoCodeFromBytesAndOffset(parser.read(3), 0);// The isoCode
+ interestStart = RippleDate.fromParser(parser);
+ long l = UInt64.fromParser(parser).longValue();
+ interestRate = Double.longBitsToDouble(l);
+ }
+ }
+ public Demurrage demurrage = null;
+ public Currency(byte[] bytes) {
+ super(bytes);
+ type = Type.fromByte(this.hash[0]);
+ if (type == Type.DEMURRAGE) {
+ demurrage = new Demurrage(bytes);
+ }
+ }
+
+ /**
+ * It's better to extend HashTranslator than the Hash160.Translator directly
+ * That way the generics can still vibe with the @Override
+ */
+ private static class CurrencyTranslator extends HashTranslator {
+ @Override
+ public int byteWidth() {
+ return 20;
+ }
+
+ @Override
+ public Currency newInstance(byte[] b) {
+ return new Currency(b);
+ }
+
+ @Override
+ public Object toJSON(Currency obj) {
+ return obj.toString();
+ }
+
+ @Override
+ public Currency fromString(String value) {
+ if (value.length() == 40 /* byteWidth() * 2 */) {
+ return newInstance(B16.decode(value));
+ } else if (value.equals("XRP")) {
+ return XRP;
+ } else {
+ if (!(value.length() == 3)) {
+// if (!value.matches("[A-Z0-9]{3}")) {
+ throw new RuntimeException("Currency code must be 3 characters");
+ }
+ return newInstance(encodeCurrency(value));
+ }
+ }
+ }
+
+ public static Currency fromString(String currency) {
+ return translate.fromString(currency);
+ }
+
+ public static Currency fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ @Override
+ public String toString() {
+ switch (type) {
+ case ISO:
+ String code = getCurrencyCodeFromTLCBytes(bytes());
+ if (code.equals("XRP")) {
+ // HEX of the bytes
+ return super.toString();
+ } else if (code.equals("\0\0\0")) {
+ return "XRP";
+ } else {
+ // the 3 letter isoCode
+ return code;
+ }
+ case HASH:
+ case DEMURRAGE:
+ case UNKNOWN:
+ default:
+ return super.toString();
+ }
+ }
+
+ public String humanCode() {
+ if (type == Type.ISO) {
+ return getCurrencyCodeFromTLCBytes(hash);
+ } else if (type == Type.DEMURRAGE) {
+ return isoCodeFromBytesAndOffset(hash, 1);
+ } else {
+ throw new IllegalStateException("No human code for currency of type " + type);
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Currency) {
+ Currency other = (Currency) obj;
+ byte[] bytes = this.bytes();
+ byte[] otherBytes = other.bytes();
+
+ if (type == Type.ISO && other.type == Type.ISO) {
+ return (bytes[12] == otherBytes[12] &&
+ bytes[13] == otherBytes[13] &&
+ bytes[14] == otherBytes[14]);
+ }
+ }
+ return super.equals(obj); // Full comparison
+ }
+
+ private static CurrencyTranslator translate = new CurrencyTranslator();
+
+ /*
+ * The following are static methods, legacy from when there was no
+ * usage of Currency objects, just String with "XRP" ambiguity.
+ * */
+ public static byte[] encodeCurrency(String currencyCode) {
+ byte[] currencyBytes = new byte[20];
+ currencyBytes[12] = (byte) currencyCode.codePointAt(0);
+ currencyBytes[13] = (byte) currencyCode.codePointAt(1);
+ currencyBytes[14] = (byte) currencyCode.codePointAt(2);
+ return currencyBytes;
+ }
+
+ public static String getCurrencyCodeFromTLCBytes(byte[] bytes) {
+ int i;
+ boolean zeroInNonCurrencyBytes = true;
+
+ for (i = 0; i < 20; i++) {
+ zeroInNonCurrencyBytes = zeroInNonCurrencyBytes &&
+ ((i == 12 || i == 13 || i == 14) || // currency bytes (0 or any other)
+ bytes[i] == 0); // non currency bytes (0)
+ }
+
+ if (zeroInNonCurrencyBytes) {
+ return isoCodeFromBytesAndOffset(bytes, 12);
+ } else {
+ throw new IllegalStateException("Currency is invalid");
+ }
+ }
+
+ private static char charFrom(byte[] bytes, int i) {
+ return (char) bytes[i];
+ }
+
+ private static String isoCodeFromBytesAndOffset(byte[] bytes, int offset) {
+ char a = charFrom(bytes, offset);
+ char b = charFrom(bytes, offset + 1);
+ char c = charFrom(bytes, offset + 2);
+ return "" + a + b + c;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Flags.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Flags.java
new file mode 100644
index 0000000000..9b0c319dcf
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Flags.java
@@ -0,0 +1,35 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+
+import java.util.BitSet;
+
+// TODO
+public class Flags extends BitSet implements SerializedType {
+ @Override
+ public Object toJSON() {
+ return null;
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return new byte[0];
+ }
+
+ @Override
+ public String toHex() {
+ return null;
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt32;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Issue.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Issue.java
new file mode 100644
index 0000000000..44f75dae82
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Issue.java
@@ -0,0 +1,101 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.coretypes.hash.Hash160;
+import org.json.JSONObject;
+
+import java.math.BigDecimal;
+
+/**
+ * Represents a currency/issuer pair
+ */
+public class Issue implements Comparable {
+
+ public static final Issue XRP = fromString("XRP");
+ final Currency currency;
+ final AccountID issuer;
+
+ public Issue(Currency currency, AccountID issuer) {
+ this.currency = currency;
+ this.issuer = issuer;
+ }
+
+ public static Issue fromString(String pair) {
+ String[] split = pair.split("/");
+ return fromStringPair(split);
+ }
+
+ private static Issue fromStringPair(String[] split) {
+ if (split.length == 2) {
+ return new Issue(Currency.fromString(split[0]), AccountID.fromString(split[1]));
+ } else if (split[0].equals("XRP")) {
+ return new Issue(Currency.XRP, AccountID.XRP_ISSUER);
+ } else {
+ throw new RuntimeException("Issue string must be XRP or $currency/$issuer");
+ }
+ }
+
+ /**
+ * See {@link com.ripple.core.fields.Field#TakerGetsCurrency}
+ * See {@link com.ripple.core.fields.Field#TakerGetsIssuer}
+ *
+ * TODO: better handling of Taker(Gets|Pays)(Issuer|Curency)
+ * maybe special subclasses of AccountID / Currency
+ * respectively?
+ */
+ public static Issue from160s(Hash160 currency, Hash160 issuer) {
+ return new Issue(new Currency(currency.bytes()),
+ new AccountID(issuer.toBytes()));
+ }
+
+ public Currency currency() {
+ return currency;
+ }
+
+ public AccountID issuer() {
+ return issuer;
+ }
+
+ @Override
+ public String toString() {
+ if (isNative()) {
+ return "XRP";
+ } else {
+ return String.format("%s/%s", currency, issuer);
+ }
+ }
+
+ public JSONObject toJSON() {
+ JSONObject o = new JSONObject();
+ o.put("currency", currency);
+ if (!isNative()) {
+ o.put("issuer", issuer);
+ }
+ return o;
+ }
+
+ public Amount amount(BigDecimal value) {
+ return new Amount(value, currency, issuer, isNative());
+ }
+
+ public boolean isNative() {
+ return this == XRP || currency.equals(Currency.XRP);
+ }
+
+ public Amount amount(Number value) {
+ return new Amount(BigDecimal.valueOf(value.doubleValue()), currency, issuer, isNative());
+ }
+
+ @Override
+ public int compareTo(Issue o) {
+ int ret = issuer.compareTo(o.issuer);
+ if (ret != 0) {
+ return ret;
+ }
+ ret = currency.compareTo(o.currency);
+ return ret;
+ }
+
+ public Amount roundedAmount(BigDecimal amount) {
+ return amount(Amount.roundValue(amount, isNative()));
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/IssuePair.java b/ripple-core/src/main/java/com/ripple/core/coretypes/IssuePair.java
new file mode 100644
index 0000000000..0196e67f86
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/IssuePair.java
@@ -0,0 +1,36 @@
+package com.ripple.core.coretypes;
+
+import org.json.JSONObject;
+
+import java.text.MessageFormat;
+
+public class IssuePair implements Comparable {
+ public final Issue pays;
+ public final Issue gets;
+
+ public IssuePair(Issue pays, Issue gets) {
+ this.pays = pays;
+ this.gets = gets;
+ }
+
+ @Override
+ public String toString() {
+ return MessageFormat.format("{0}/{1}", pays, gets);
+ }
+
+ @Override
+ public int compareTo(IssuePair o) {
+ int cmp = pays.compareTo(o.pays);
+ if (cmp == 0) {
+ cmp = gets.compareTo(o.gets);
+ }
+ return cmp;
+ }
+
+ public JSONObject toJSON() {
+ JSONObject ret = new JSONObject();
+ ret.put("taker_gets", gets.toJSON());
+ ret.put("taker_pays", pays.toJSON());
+ return ret;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/PathSet.java b/ripple-core/src/main/java/com/ripple/core/coretypes/PathSet.java
new file mode 100644
index 0000000000..b054229787
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/PathSet.java
@@ -0,0 +1,273 @@
+package com.ripple.core.coretypes;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.PathSetField;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class PathSet extends ArrayList implements SerializedType {
+ public static byte PATH_SEPARATOR_BYTE = (byte) 0xFF;
+ public static byte PATHSET_END_BYTE = (byte) 0x00;
+
+ public PathSet(){}
+
+ public static PathSet fromJSONArray(JSONArray array) {
+ return translate.fromJSONArray(array);
+ }
+
+ public static PathSet fromHex(String hex) {
+ return translate.fromHex(hex);
+ }
+
+ public static class Hop {
+ public static byte TYPE_ACCOUNT = (byte) 0x01;
+ public static byte TYPE_CURRENCY = (byte) 0x10;
+ public static byte TYPE_ISSUER = (byte) 0x20;
+ public static final int TYPE_ACCOUNT_CURRENCY_ISSUER = TYPE_CURRENCY | TYPE_ACCOUNT | TYPE_ISSUER;
+ public static final int TYPE_ACCOUNT_CURRENCY = TYPE_CURRENCY | TYPE_ACCOUNT;
+ public static int VALID_TYPE_MASK = ~(TYPE_ACCOUNT | TYPE_CURRENCY | TYPE_ISSUER);
+
+ public AccountID account;
+ public AccountID issuer;
+ public Currency currency;
+ private int type;
+
+ public boolean hasIssuer() {
+ return issuer != null;
+ }
+ public boolean hasCurrency() {
+ return currency != null;
+ }
+ public boolean hasAccount() {
+ return account != null;
+ }
+
+ public int getType() {
+ if (type == 0) {
+ synthesizeType();
+ }
+ return type;
+ }
+
+ static public Hop fromJSONObject(JSONObject json) {
+ Hop hop = new Hop();
+ if (json.has("account")) {
+ hop.account = AccountID.fromAddress(json.getString("account"));
+ }
+ if (json.has("issuer")) {
+ hop.issuer = AccountID.fromAddress(json.getString("issuer"));
+ }
+ if (json.has("currency")) {
+ hop.currency = Currency.fromString(json.getString("currency"));
+ }
+ if (json.has("type")) {
+ hop.type = json.getInt("type");
+ }
+ return hop;
+ }
+
+ public void synthesizeType() {
+ type = 0;
+
+ if (hasAccount()) type |= TYPE_ACCOUNT;
+ if (hasCurrency()) type |= TYPE_CURRENCY;
+ if (hasIssuer()) type |= TYPE_ISSUER;
+ }
+
+ public JSONObject toJSONObject() {
+ JSONObject object = new JSONObject();
+ object.put("type", getType());
+
+ if (hasAccount()) object.put("account", account.toJSON());
+ if (hasIssuer()) object.put("issuer", issuer.toJSON());
+ if (hasCurrency()) object.put("currency", currency.toJSON());
+ return object;
+ }
+
+ public static Hop fromJacksonObject(ObjectNode json) {
+ Hop hop = new Hop();
+ if (json.has("account")) {
+ hop.account = AccountID.fromAddress(json.get("account").asText());
+ }
+ if (json.has("issuer")) {
+ hop.issuer = AccountID.fromAddress(json.get("issuer").asText());
+ }
+ if (json.has("currency")) {
+ hop.currency = Currency.fromString(json.get("currency").asText());
+ }
+ if (json.has("type")) {
+ hop.type = json.get("type").asInt();
+ }
+ return hop;
+
+ }
+ }
+ public static class Path extends ArrayList {
+ static public Path fromJSONArray(JSONArray array) {
+ Path path = new Path();
+ int nHops = array.length();
+ for (int i = 0; i < nHops; i++) {
+ JSONObject hop = array.getJSONObject(i);
+ path.add(Hop.fromJSONObject(hop));
+ }
+
+ return path;
+ }
+ static public Path fromJacksonArray(ArrayNode array) {
+ Path path = new Path();
+ int nHops = array.size();
+ for (int i = 0; i < nHops; i++) {
+ ObjectNode hop = (ObjectNode) array.get(i);
+ path.add(Hop.fromJacksonObject(hop));
+ }
+ return path;
+ }
+ public JSONArray toJSONArray() {
+ JSONArray array = new JSONArray();
+ for (Hop hop : this) {
+ array.put(hop.toJSONObject());
+ }
+ return array;
+ }
+ }
+
+ public JSONArray toJSONArray() {
+ JSONArray array = new JSONArray();
+ for (Path path : this) {
+ array.put(path.toJSONArray());
+ }
+ return array;
+ }
+
+ // SerializedType interface implementation
+ @Override
+ public Object toJSON() {
+ return toJSONArray();
+ }
+
+ @Override
+ public void toBytesSink(BytesSink buffer) {
+ int n = 0;
+ for (Path path : this) {
+ if (n++ != 0) {
+ buffer.add(PATH_SEPARATOR_BYTE);
+ }
+ for (Hop hop : path) {
+ int type = hop.getType();
+ buffer.add((byte) type);
+ if (hop.hasAccount()) {
+ buffer.add(hop.account.bytes());
+ }
+ if (hop.hasCurrency()) {
+ buffer.add(hop.currency.bytes());
+ }
+ if (hop.hasIssuer()) {
+ buffer.add(hop.issuer.bytes());
+ }
+ }
+ }
+ buffer.add(PATHSET_END_BYTE);
+ }
+
+ @Override
+ public Type type() {
+ return Type.PathSet;
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public PathSet fromParser(BinaryParser parser, Integer hint) {
+ PathSet pathSet = new PathSet();
+ PathSet.Path path = null;
+ while (!parser.end()) {
+ byte type = parser.readOne();
+ if (type == PATHSET_END_BYTE) {
+ break;
+ }
+ if (path == null) {
+ path = new PathSet.Path();
+ pathSet.add(path);
+ }
+ if (type == PATH_SEPARATOR_BYTE) {
+ path = null;
+ continue;
+ }
+
+ PathSet.Hop hop = new PathSet.Hop();
+ path.add(hop);
+ if ((type & Hop.TYPE_ACCOUNT) != 0) {
+ hop.account = AccountID.fromParser(parser);
+ }
+ if ((type & Hop.TYPE_CURRENCY) != 0) {
+ hop.currency = Currency.fromParser(parser);
+ }
+ if ((type & Hop.TYPE_ISSUER) != 0) {
+ hop.issuer = AccountID.fromParser(parser);
+ }
+ }
+
+ return pathSet;
+ }
+
+ @Override
+ public PathSet fromJSONArray(JSONArray array) {
+ PathSet paths = new PathSet();
+
+ int nPaths = array.length();
+
+ for (int i = 0; i < nPaths; i++) {
+ JSONArray path = array.getJSONArray(i);
+ paths.add(Path.fromJSONArray(path));
+ }
+ return paths;
+
+ }
+
+ @Override
+ public PathSet fromJacksonArray(ArrayNode array) {
+ PathSet paths = new PathSet();
+
+ int nPaths = array.size();
+
+ for (int i = 0; i < nPaths; i++) {
+ ArrayNode path = (ArrayNode) array.get(i);
+ paths.add(Path.fromJacksonArray(path));
+ }
+
+ return paths;
+ }
+ }
+ static public Translator translate = new Translator();
+
+ public static PathSet fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+ public static PathSet fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ private static PathSetField pathsetField(final Field f) {
+ return new PathSetField(){ @Override public Field getField() {return f;}};
+ }
+ static public PathSetField Paths = pathsetField(Field.Paths);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Quality.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Quality.java
new file mode 100644
index 0000000000..212da6d78e
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Quality.java
@@ -0,0 +1,26 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.coretypes.hash.Hash256;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public class Quality {
+ /**
+ * Finds the quality (TakerPays/TakerGets) ratio packed into the last 64
+ * bits of root DirectoryNode ledger indexes.
+ */
+ public static BigDecimal fromBookDirectory(Hash256 bookDirectory,
+ boolean payIsNative,
+ boolean getIsNative) {
+ // The last 7 bytes contains the mantissa
+ byte[] mantissa = bookDirectory.slice(-7);
+ // Most significant byte has the exponent packed
+ int exponent = ( bookDirectory.get(-8) & 0xFF) - 100;
+ // Return the value in XRP scale, rather than drops, as stored.
+ int scale = -(payIsNative ? exponent - 6 :
+ getIsNative ? exponent + 6 : exponent);
+ BigInteger unsignedBig = new BigInteger(1, mantissa);
+ return new BigDecimal(unsignedBig, scale);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/RippleDate.java b/ripple-core/src/main/java/com/ripple/core/coretypes/RippleDate.java
new file mode 100644
index 0000000000..71a94e1ce9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/RippleDate.java
@@ -0,0 +1,82 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.BinaryParser;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+//public class RippleDate extends Date implements SerializedType {
+public class RippleDate extends Date {
+ private static final SimpleDateFormat sdf = new SimpleDateFormat();
+
+ public static long RIPPLE_EPOCH_SECONDS_OFFSET = 0x386D4380;
+ static {
+ sdf.setTimeZone(new SimpleTimeZone(0, "GMT"));
+ sdf.applyPattern("dd MMM yyyy HH:mm:ss z");
+
+ /**
+ * Magic constant tested and documented.
+ *
+ * Seconds since the unix epoch from unix time (accounting leap years etc)
+ * at 1/January/2000 GMT
+ */
+ GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+ cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
+ long computed = cal.getTimeInMillis() / 1000;
+ assertEquals("01 Jan 2000 00:00:00 GMT", sdf.format(cal.getTime())); // TODO
+ assertEquals(RippleDate.RIPPLE_EPOCH_SECONDS_OFFSET, computed);
+ }
+
+ static public String gmtString(Date date) {
+ return sdf.format(date);
+ }
+
+ private static void assertEquals(String s, String s1) {
+ if (!s.equals(s1)) throw new AssertionError(String.format("%s != %s", s, s1));
+ }
+ private static void assertEquals(long a, long b) {
+ if (a != b) throw new AssertionError(String.format("%s != %s", a, b));
+ }
+
+ private RippleDate() {
+ super();
+ }
+ private RippleDate(long milliseconds) {
+ super(milliseconds);
+ }
+
+ public long secondsSinceRippleEpoch() {
+ return ((this.getTime() / 1000) - RIPPLE_EPOCH_SECONDS_OFFSET);
+ }
+ public static RippleDate fromSecondsSinceRippleEpoch(Number seconds) {
+ return new RippleDate((seconds.longValue() + RIPPLE_EPOCH_SECONDS_OFFSET) * 1000);
+ }
+ public static RippleDate fromParser(BinaryParser parser) {
+ UInt32 uInt32 = UInt32.fromParser(parser);
+ return fromSecondsSinceRippleEpoch(uInt32);
+ }
+ public static RippleDate now() {
+ return new RippleDate();
+ }
+
+/* @Override
+ public Object toJSON() {
+ return secondsSinceRippleEpoch();
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return new UInt32(secondsSinceRippleEpoch()).toBytes();
+ }
+
+ @Override
+ public String toHex() {
+ return new UInt32(secondsSinceRippleEpoch()).toHex();
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ new UInt32(secondsSinceRippleEpoch()).toBytesSink(to);
+ }*/
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/STArray.java b/ripple-core/src/main/java/com/ripple/core/coretypes/STArray.java
new file mode 100644
index 0000000000..6e7b0191f3
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/STArray.java
@@ -0,0 +1,129 @@
+package com.ripple.core.coretypes;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.STArrayField;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class STArray extends ArrayList implements SerializedType {
+
+ public JSONArray toJSONArray() {
+ JSONArray array = new JSONArray();
+
+ for (STObject so : this) {
+ array.put(so.toJSON());
+ }
+
+ return array;
+ }
+
+ @Override
+ public Object toJSON() {
+ return toJSONArray();
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ for (STObject stObject : this) {
+ stObject.toBytesSink(to);
+ }
+ }
+
+ @Override
+ public Type type() {
+ return Type.STArray;
+ }
+
+ public static STArray fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+ public static STArray fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+ public static STArray fromHex(String hex) {
+ return translate.fromHex(hex);
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public STArray fromParser(BinaryParser parser, Integer hint) {
+ STArray stArray = new STArray();
+ while (!parser.end()) {
+ Field field = parser.readField();
+ if (field == Field.ArrayEndMarker) {
+ break;
+ }
+ STObject outer = new STObject();
+ // assert field.getType() == Type.STObject;
+ outer.put(field, STObject.fromParser(parser));
+ stArray.add(STObject.formatted(outer));
+ }
+ return stArray;
+ }
+
+ public JSONArray toJSONArray(STArray obj) {
+ return obj.toJSONArray();
+ }
+
+ @Override
+ public STArray fromJSONArray(JSONArray jsonArray) {
+ STArray arr = new STArray();
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ Object o = jsonArray.get(i);
+ arr.add(STObject.fromJSONObject((JSONObject) o));
+ }
+
+ return arr;
+ }
+ @Override
+ public STArray fromJacksonArray(ArrayNode jsonArray) {
+ STArray arr = new STArray();
+
+ for (int i = 0; i < jsonArray.size(); i++) {
+ ObjectNode object = (ObjectNode) jsonArray.get(i);
+ arr.add(STObject.fromJacksonObject(object));
+ }
+
+ return arr;
+ }
+ }
+ static public Translator translate = new Translator();
+
+ public STArray(){}
+
+ private static STArrayField starrayField(final Field f) {
+ return new STArrayField(){ @Override public Field getField() {return f;}};
+ }
+
+ static public STArrayField AffectedNodes = starrayField(Field.AffectedNodes);
+ static public STArrayField SignerEntries = starrayField(Field.SignerEntries);
+ static public STArrayField Signers = starrayField(Field.Signers);
+
+ static public STArrayField Template = starrayField(Field.Template);
+ static public STArrayField Necessary = starrayField(Field.Necessary);
+ static public STArrayField Sufficient = starrayField(Field.Sufficient);
+ static public STArrayField Majorities = starrayField(Field.Majorities);
+ static public STArrayField Memos = starrayField(Field.Memos);
+ static public STArrayField ArrayEndMarker = starrayField(Field.ArrayEndMarker);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/STObject.java b/ripple-core/src/main/java/com/ripple/core/coretypes/STObject.java
new file mode 100644
index 0000000000..b02988c082
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/STObject.java
@@ -0,0 +1,489 @@
+package com.ripple.core.coretypes;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.coretypes.hash.HalfSha512;
+import com.ripple.core.coretypes.hash.Hash128;
+import com.ripple.core.coretypes.hash.Hash160;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.uint.UInt16;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.coretypes.uint.UInt8;
+import com.ripple.core.fields.*;
+import com.ripple.core.formats.Format;
+import com.ripple.core.formats.LEFormat;
+import com.ripple.core.formats.TxFormat;
+import com.ripple.core.serialized.*;
+import com.ripple.core.serialized.enums.EngineResult;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.serialized.enums.TransactionType;
+import org.json.JSONObject;
+
+import java.util.EnumMap;
+import java.util.Iterator;
+import java.util.TreeMap;
+import java.util.function.Predicate;
+
+public class STObject implements SerializedType, Iterable {
+ // Internally the fields are stored in a TreeMap
+ public static class FieldsMap extends TreeMap {}
+// public static class FieldsMap extends HashMap {}
+
+ protected FieldsMap fields;
+ public Format format;
+
+ public STObject() {
+ fields = new FieldsMap();
+ }
+ public STObject(FieldsMap fieldsMap) {
+ fields = fieldsMap;
+ }
+
+ public static STObject fromJSON(String json) {
+ return fromJSONObject(new JSONObject(json));
+ }
+ public static STObject fromJSONObject(JSONObject json) {
+ return translate.fromJSONObject(json);
+ }
+ public static STObject fromJacksonObject(ObjectNode object) {
+ return translate.fromJacksonObject(object);
+ }
+ public static STObject fromHex(String hex) {
+ return STObject.translate.fromHex(hex);
+ }
+ public static STObject fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+ public static STObject fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+ public static STObject fromParser(BinaryParser parser, Integer hint) {
+ return translate.fromParser(parser, hint);
+ }
+
+ @Override
+ public Iterator iterator() {
+// return fields.keySet().stream().sorted().iterator();
+ return fields.keySet().iterator();
+ }
+
+ public String prettyJSON() {
+ return translate.toJSONObject(this).toString(2);
+ }
+
+ /**
+ * @return a subclass of STObject using the same fields
+ *
+ * If the object has a TransactionType or LedgerEntryType
+ * then we can up)grade to a child class, with more specific
+ * helper methods, and we can use `instanceof` to great effect.
+ */
+ public static STObject formatted(STObject source) {
+ return STObjectFormatter.format(source);
+
+ }
+
+ public Format getFormat() {
+ if (format == null) computeFormat();
+ return format;
+ }
+
+ public static class FormatException extends RuntimeException {
+ FormatException(String s) {
+ super(s);
+ }
+ }
+
+ public void checkFormat() {
+ Format> fmt = getFormat();
+ EnumMap requirements = fmt.requirements();
+ for (Field field : this) {
+ if (!requirements.containsKey(field)) {
+ throw new FormatException(fmt.name() +
+ " doesn't have field: " + field);
+ }
+ }
+ for (Field field : requirements.keySet()) {
+ Format.Requirement req = requirements.get(field);
+ if (!has(field)) {
+ if (req == Format.Requirement.REQUIRED) {
+ throw new FormatException(fmt.name() +
+ " requires " + field + " of type "
+ + field.getType());
+ }
+ } else {
+ SerializedType type = get(field);
+ if (type.type() != field.getType()) {
+ if (!(field.getType() == Type.Hash160 &&
+ type.type() == Type.AccountID)) {
+ throw new FormatException(type.toString() +
+ " is not " + field.getType());
+ }
+ }
+ }
+ }
+ }
+
+ public void setFormat(Format format) {
+ this.format = format;
+ }
+
+ private void computeFormat() {
+ UInt16 tt = get(UInt16.TransactionType);
+ if (tt != null) {
+ setFormat(TxFormat.fromNumber(tt));
+ }
+ UInt16 let = get(UInt16.LedgerEntryType);
+ if (let != null) {
+ setFormat(LEFormat.fromNumber(let));
+ }
+ }
+
+ public FieldsMap getFields() {
+ return fields;
+ }
+
+ public SerializedType get(Field field) {
+ return fields.get(field);
+ }
+
+ protected Hash256 signingHash(HashPrefix txSign) {
+ HalfSha512 signing = HalfSha512.prefixed256(txSign);
+ toBytesSink(signing, Field::isSigningField);
+ return signing.finish();
+ }
+
+ protected byte[] signingData(HashPrefix txSign) {
+ BytesList bl = new BytesList();
+ bl.add(txSign.bytes());
+ toBytesSink(bl, Field::isSigningField);
+ return bl.bytes();
+ }
+
+ protected static EngineResult engineResult(STObject obj) {
+ return (EngineResult) obj.get(Field.TransactionResult);
+ }
+
+ static public LedgerEntryType ledgerEntryType(STObject obj) {
+ return (LedgerEntryType) obj.get(Field.LedgerEntryType);
+ }
+
+ public static TransactionType transactionType(STObject obj) {
+ return (TransactionType) obj.get(Field.TransactionType);
+ }
+
+ public SerializedType remove(Field f) {
+ return fields.remove(f);
+ }
+
+ public boolean has(Field f) {
+ return fields.containsKey(f);
+ }
+
+ public boolean has(T hf) {
+ return has(hf.getField());
+ }
+
+ public void put (UInt8Field f, UInt8 o) {put(f.getField(), o);}
+ public void put (Vector256Field f, Vector256 o) {put(f.getField(), o);}
+ public void put (BlobField f, Blob o) {put(f.getField(), o);}
+ public void put (UInt64Field f, UInt64 o) {put(f.getField(), o);}
+ public void put (UInt32Field f, UInt32 o) {put(f.getField(), o);}
+ public void put (UInt16Field f, UInt16 o) {put(f.getField(), o);}
+ public void put (PathSetField f, PathSet o) {put(f.getField(), o);}
+ public void put (STObjectField f, STObject o) {put(f.getField(), o);}
+ public void put (Hash256Field f, Hash256 o) {put(f.getField(), o);}
+ public void put (Hash160Field f, Hash160 o) {put(f.getField(), o);}
+ public void put (Hash128Field f, Hash128 o) {put(f.getField(), o);}
+ public void put (STArrayField f, STArray o) {put(f.getField(), o);}
+ public void put (AmountField f, Amount o) {put(f.getField(), o);}
+ public void put (AccountIDField f, AccountID o) {put(f.getField(), o);}
+
+ public void putTranslated(T f, Object value) {
+ putTranslated(f.getField(), value);
+ }
+
+ public STObject as(T f, Object value) {
+ putTranslated(f.getField(), value);
+ return this;
+ }
+
+ public void put(Field f, SerializedType value) {
+ fields.put(f, value);
+ }
+
+ public void putTranslated(Field f, Object value) {
+ TypeTranslator typeTranslator = Translators.forField(f);
+ SerializedType st;
+ try {
+ st = typeTranslator.fromValue(value);
+ } catch (Exception e) {
+ throw new RuntimeException("Couldn't put `" +value+ "` into field `" + f + "`", e);
+ }
+ fields.put(f, st);
+ }
+
+ public AccountID get(AccountIDField f) {
+ return (AccountID) get(f.getField());
+ }
+
+ public Amount get(AmountField f) {
+ return (Amount) get(f.getField());
+ }
+
+ public STArray get(STArrayField f) {
+ return (STArray) get(f.getField());
+ }
+
+ public Hash128 get(Hash128Field f) {
+ return (Hash128) get(f.getField());
+ }
+
+ public Hash160 get(Hash160Field f) {
+ return (Hash160) get(f.getField());
+ }
+
+ public Hash256 get(Hash256Field f) {
+ return (Hash256) get(f.getField());
+ }
+
+ public STObject get(STObjectField f) {
+ return (STObject) get(f.getField());
+ }
+
+ public PathSet get(PathSetField f) {
+ return (PathSet) get(f.getField());
+ }
+
+ public UInt16 get(UInt16Field f) {
+ return (UInt16) get(f.getField());
+ }
+
+ public UInt32 get(UInt32Field f) {
+ return (UInt32) get(f.getField());
+ }
+
+ public UInt64 get(UInt64Field f) {
+ return (UInt64) get(f.getField());
+ }
+
+ public UInt8 get(UInt8Field f) {
+ return (UInt8) get(f.getField());
+ }
+
+ public Vector256 get(Vector256Field f) {
+ return (Vector256) get(f.getField());
+ }
+
+ public Blob get(BlobField f) {
+ return (Blob) get(f.getField());
+ }
+
+ // SerializedTypes implementation
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ public JSONObject toJSONObject() {
+ return translate.toJSONObject(this);
+ }
+
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ public void toBytesSink(BytesSink to, Predicate p) {
+ BinarySerializer serializer = new BinarySerializer(to);
+
+ for (Field field : this) {
+ if (p.test(field)) {
+ SerializedType value = fields.get(field);
+ serializer.add(field, value);
+ }
+ }
+ }
+ @Override
+ public void toBytesSink(BytesSink to) {
+ toBytesSink(to, field -> field.isSerialized());
+ }
+
+ @Override
+ public Type type() {
+ return Type.STObject;
+ }
+
+ private static class Translator extends TypeTranslator {
+
+ @Override
+ public STObject fromParser(BinaryParser parser, Integer hint) {
+ STObject so = new STObject();
+ TypeTranslator tr;
+ SerializedType st;
+ Field field;
+ Integer sizeHint;
+
+ // hint, is how many bytes to parse
+ if (hint != null) {
+ // end hint
+ hint = parser.pos() + hint;
+ }
+
+ while (!parser.end(hint)) {
+ field = parser.readField();
+ if (field == Field.ObjectEndMarker) {
+ break;
+ }
+ tr = Translators.forField(field);
+ sizeHint = field.isVLEncoded() ? parser.readVLLength() : null;
+ st = tr.fromParser(parser, sizeHint);
+ if (st == null) {
+ throw new IllegalStateException("Parsed " + field + " as null");
+ }
+ so.put(field, st);
+ }
+
+ return STObject.formatted(so);
+ }
+
+ @Override
+ public Object toJSON(STObject obj) {
+ return toJSONObject(obj);
+ }
+
+ public JSONObject toJSONObject(STObject obj) {
+ JSONObject json = new JSONObject();
+
+ for (Field f : obj) {
+ SerializedType obj1 = obj.get(f);
+ Object object = obj1.toJSON();
+ json.put(f.name(), object);
+ }
+
+ return json;
+ }
+
+ @Override
+ public STObject fromJSONObject(JSONObject jsonObject) {
+ STObject so = new STObject();
+
+ Iterator keys = jsonObject.keys();
+ while (keys.hasNext()) {
+ String key = (String) keys.next();
+ Object value = jsonObject.get(key);
+ Field fieldKey = Field.fromString(key);
+ if (fieldKey == null) {
+ continue;
+ }
+ so.putTranslated(fieldKey, value);
+ }
+ return STObject.formatted(so);
+ }
+
+ @Override
+ public STObject fromJacksonObject(ObjectNode object) {
+ STObject so = new STObject();
+
+ Iterator keys = object.fieldNames();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ JsonNode value = object.get(key);
+ Field fieldKey = Field.fromString(key);
+ if (fieldKey == null) {
+ continue;
+ }
+ so.putTranslated(fieldKey, value);
+ }
+ return STObject.formatted(so);
+ }
+ }
+
+ public int size() {
+ return fields.size();
+ }
+
+ static private Translator translate = new Translator();
+
+ private static STObjectField stobjectField(final Field f) {
+ return new STObjectField() {@Override public Field getField() {return f; } };
+ }
+
+ static public STObjectField TransactionMetaData = stobjectField(Field.TransactionMetaData);
+ static public STObjectField CreatedNode = stobjectField(Field.CreatedNode);
+ static public STObjectField DeletedNode = stobjectField(Field.DeletedNode);
+ static public STObjectField ModifiedNode = stobjectField(Field.ModifiedNode);
+ static public STObjectField PreviousFields = stobjectField(Field.PreviousFields);
+ static public STObjectField FinalFields = stobjectField(Field.FinalFields);
+ static public STObjectField NewFields = stobjectField(Field.NewFields);
+ static public STObjectField TemplateEntry = stobjectField(Field.TemplateEntry);
+ static public STObjectField Signer = stobjectField(Field.Signer);
+ static public STObjectField SignerEntry = stobjectField(Field.SignerEntry);
+ static public STObjectField ObjectEndMarker = stobjectField(Field.ObjectEndMarker);
+ static public STObjectField Memo = stobjectField(Field.Memo);
+ static public STObjectField Majority = stobjectField(Field.Majority);
+
+ private static class Translators {
+ @SuppressWarnings("unused")
+ private static TypeTranslator get(Class extends SerializedType> kls) {
+ try {
+ java.lang.reflect.Field translate = kls.getDeclaredField("translate");
+ translate.setAccessible(true);
+ return (TypeTranslator) translate.get(kls);
+ } catch (Exception e) {
+ throw new RuntimeException("for kls: " + kls.getSimpleName(), e);
+ }
+ }
+
+ private static TypeTranslator forType(Type type) {
+ switch (type) {
+ case STObject: return STObject.translate; // get(STObject.class);
+ case Amount: return Amount.translate; // get(Amount.class);
+ case UInt16: return UInt16.translate; // get(UInt16.class);
+ case UInt32: return UInt32.translate; // get(UInt32.class);
+ case UInt64: return UInt64.translate; // get(UInt64.class);
+ case Hash128: return Hash128.translate; // get(Hash128.class);
+ case Hash256: return Hash256.translate; // get(Hash256.class);
+ case Blob: return Blob.translate; // get(Blob.class);
+ case AccountID: return AccountID.translate; // get(AccountID.class);
+ case STArray: return STArray.translate; // get(STArray.class);
+ case UInt8: return UInt8.translate; // get(UInt8.class);
+ case Hash160: return Hash160.translate; // get(Hash160.class);
+ case PathSet: return PathSet.translate; // get(PathSet.class);
+ case Vector256: return Vector256.translate; // get(Vector256.class);
+ default: throw new IllegalStateException("Unknown type");
+ }
+ }
+
+ private static TypeTranslator forField(Field field) {
+ if (field.tag == null) {
+ switch (field) {
+ case LedgerEntryType:
+ field.tag = LedgerEntryType.translate; //get(LedgerEntryType.class);
+ break;
+ case TransactionType:
+ field.tag = TransactionType.translate;// get(TransactionType.class);
+ break;
+ case TransactionResult:
+ field.tag = EngineResult.translate; // get(EngineResult.class);
+ break;
+ default:
+ field.tag = forType(field.getType());
+ break;
+ }
+ }
+ return getCastedTag(field);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static TypeTranslator getCastedTag(Field field) {
+ return (TypeTranslator) field.tag;
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/STObjectFormatter.java b/ripple-core/src/main/java/com/ripple/core/coretypes/STObjectFormatter.java
new file mode 100644
index 0000000000..d17b6ae0ee
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/STObjectFormatter.java
@@ -0,0 +1,170 @@
+package com.ripple.core.coretypes;
+
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.generic.Validation;
+import com.ripple.core.types.known.sle.LedgerHashes;
+import com.ripple.core.types.known.sle.entries.*;
+import com.ripple.core.types.known.tx.result.AffectedNode;
+import com.ripple.core.types.known.tx.result.TransactionMeta;
+import com.ripple.core.types.known.tx.txns.*;
+import com.ripple.core.types.known.tx.txns.pseudo.EnableAmendment;
+import com.ripple.core.types.known.tx.txns.pseudo.SetFee;
+
+public class STObjectFormatter {
+ public static STObject format(STObject source) {
+ // This would need to go before the test that just checks
+ // for ledgerEntryType
+ if (AffectedNode.isAffectedNode(source)) {
+ return new AffectedNode(source);
+ }
+
+ if (TransactionMeta.isTransactionMeta(source)) {
+ TransactionMeta meta = new TransactionMeta();
+ meta.fields = source.fields;
+ return meta;
+ }
+
+ LedgerEntryType ledgerEntryType = STObject.ledgerEntryType(source);
+ if (ledgerEntryType != null) {
+ return ledgerFormatted(source, ledgerEntryType);
+ }
+
+ TransactionType transactionType = STObject.transactionType(source);
+ if (transactionType != null) {
+ return transactionFormatted(source, transactionType);
+ }
+
+ if (Validation.isValidation(source)) {
+ Validation validation = new Validation();
+ validation.fields = source.fields;
+ return validation;
+ }
+
+ return source;
+ }
+
+ private static STObject transactionFormatted(STObject source, TransactionType transactionType) {
+ STObject constructed = null;
+ switch (transactionType) {
+ case Payment:
+ constructed = new Payment();
+ break;
+ case EscrowCreate:
+ constructed = new EscrowCreate();
+ break;
+ case EscrowFinish:
+ constructed = new EscrowFinish();
+ break;
+ case AccountSet:
+ constructed = new AccountSet();
+ break;
+ case EscrowCancel:
+ constructed = new EscrowCancel();
+ break;
+ case SetRegularKey:
+ constructed = new SetRegularKey();
+ break;
+ case OfferCreate:
+ constructed = new OfferCreate();
+ break;
+ case OfferCancel:
+ constructed = new OfferCancel();
+ break;
+ case TicketCreate:
+ constructed = new TicketCreate();
+ break;
+ case TicketCancel:
+ constructed = new TicketCancel();
+ break;
+ case SignerListSet:
+ constructed = new SignerListSet();
+ break;
+ case PaymentChannelCreate:
+ constructed = new PaymentChannelCreate();
+ break;
+ case PaymentChannelFund:
+ constructed = new PaymentChannelFund();
+ break;
+ case PaymentChannelClaim:
+ constructed = new PaymentChannelClaim();
+ break;
+ case CheckCreate:
+ constructed = new CheckCreate();
+ break;
+ case CheckCash:
+ constructed = new CheckCash();
+ break;
+ case CheckCancel:
+ constructed = new CheckCancel();
+ break;
+ case DepositPreauth:
+ constructed = new DepositPreauth();
+ break;
+ case TrustSet:
+ constructed = new TrustSet();
+ break;
+ case EnableAmendment:
+ constructed = new EnableAmendment();
+ break;
+ case SetFee:
+ constructed = new SetFee();
+ break;
+ }
+
+ constructed.fields = source.fields;
+ return constructed;
+
+ }
+
+ private static STObject ledgerFormatted(STObject source, LedgerEntryType ledgerEntryType) {
+ STObject constructed = null;
+ switch (ledgerEntryType) {
+ case Escrow:
+ constructed = new Escrow();
+ break;
+ case Offer:
+ constructed = new Offer();
+ break;
+ case RippleState:
+ constructed = new RippleState();
+ break;
+ case AccountRoot:
+ constructed = new AccountRoot();
+ break;
+ case DirectoryNode:
+ if (source.has(AccountID.Owner)) {
+ constructed = new OwnerDirectory();
+ } else {
+ constructed = new OfferDirectory();
+ }
+ break;
+ case LedgerHashes:
+ constructed = new LedgerHashes();
+ break;
+ case Amendments:
+ constructed = new Amendments();
+ break;
+ case FeeSettings:
+ constructed = new FeeSettings();
+ break;
+ case Ticket:
+ constructed = new Ticket();
+ break;
+ case SignerList:
+ constructed = new SignerList();
+ break;
+ case PayChannel:
+ constructed = new PayChannel();
+ break;
+ case Check:
+ constructed = new Check();
+ break;
+ case DepositPreauth:
+ constructed = new DepositPreauthLe();
+ break;
+ }
+ constructed.fields = source.fields;
+ return constructed;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/Vector256.java b/ripple-core/src/main/java/com/ripple/core/coretypes/Vector256.java
new file mode 100644
index 0000000000..8a5eef49e5
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/Vector256.java
@@ -0,0 +1,141 @@
+package com.ripple.core.coretypes;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.fields.Vector256Field;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+import org.json.JSONArray;
+
+import java.util.ArrayList;
+
+public class Vector256 extends ArrayList implements SerializedType {
+
+ @Override
+ public Object toJSON() {
+ return toJSONArray();
+ }
+
+ public JSONArray toJSONArray() {
+ JSONArray array = new JSONArray();
+
+ for (Hash256 hash256 : this) {
+ array.put(hash256.toString());
+ }
+
+ return array;
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ for (Hash256 hash256 : this) {
+ hash256.toBytesSink(to);
+ }
+ }
+
+ @Override
+ public Type type() {
+ return Type.Vector256;
+ }
+
+ /**
+ * This method puts the last element in the removed elements slot, and
+ * pops off the back, thus preserving contiguity but losing ordering.
+ * @param ledgerIndex the ledger entry index to remove
+ *
+ * Unused when the featureSortedDirectories amendment is applied:
+ * See: https://ripple.com/build/known-amendments/#sorteddirectories
+ */
+ public boolean removeUnstable(Hash256 ledgerIndex) {
+ int i = indexOf(ledgerIndex);
+ if (i == -1) {
+ return false;
+ }
+
+ int last = size() - 1;
+ Hash256 lastIndex = get(last);
+ set(i, lastIndex);
+ remove(last);
+
+ return true;
+ }
+
+ public static Vector256 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ public static Vector256 fromHex(String hex) {
+ return translate.fromHex(hex);
+ }
+ public static Vector256 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public Vector256 fromParser(BinaryParser parser, Integer hint) {
+ Vector256 vector256 = new Vector256();
+ if (hint == null) {
+ hint = parser.size() - parser.pos();
+ }
+ for (int i = 0; i < hint / 32; i++) {
+ vector256.add(Hash256.fromParser(parser));
+ }
+
+ return vector256;
+ }
+
+ public JSONArray toJSONArray(Vector256 obj) {
+ return obj.toJSONArray();
+ }
+
+ @Override
+ public Vector256 fromJSONArray(JSONArray jsonArray) {
+ Vector256 vector = new Vector256();
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ String hex = jsonArray.getString(i);
+ vector.add(new Hash256(B16.decode(hex)));
+ }
+
+ return vector;
+ }
+ @Override
+ public Vector256 fromJacksonArray(ArrayNode jsonArray) {
+ Vector256 vector = new Vector256();
+
+ for (int i = 0; i < jsonArray.size(); i++) {
+ String hex = jsonArray.get(i).asText();
+ vector.add(new Hash256(B16.decode(hex)));
+ }
+
+ return vector;
+ }
+ }
+ static public Translator translate = new Translator();
+
+ public Vector256(){}
+
+ public static Vector256Field vector256Field(final Field f) {
+ return new Vector256Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public Vector256Field Indexes = vector256Field(Field.Indexes);
+ static public Vector256Field Hashes = vector256Field(Field.Hashes);
+ static public Vector256Field Amendments = vector256Field(Field.Amendments);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/HalfSha512.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/HalfSha512.java
new file mode 100644
index 0000000000..560cce14a0
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/HalfSha512.java
@@ -0,0 +1,68 @@
+package com.ripple.core.coretypes.hash;
+
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+
+import java.security.MessageDigest;
+
+public class HalfSha512 implements BytesSink {
+ private MessageDigest messageDigest;
+
+ public HalfSha512() {
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-512");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static HalfSha512 prefixed256(Prefix bytes) {
+ HalfSha512 halfSha512 = new HalfSha512();
+ halfSha512.update(bytes);
+ return halfSha512;
+ }
+
+ public void update(byte[] bytes) {
+ messageDigest.update(bytes);
+ }
+
+ public void update(Hash256 hash) {
+ messageDigest.update(hash.bytes());
+ }
+
+ public MessageDigest digest() {
+ return messageDigest;
+ }
+
+ public Hash256 finish() {
+ byte[] half = digestBytes();
+ return new Hash256(half);
+ }
+
+ private byte[] digestBytes() {
+ byte[] digest = messageDigest.digest();
+ byte[] half = new byte[32];
+ System.arraycopy(digest, 0, half, 0, 32);
+ return half;
+ }
+
+ @Override
+ public void add(byte aByte) {
+ messageDigest.update(aByte);
+ }
+
+ @Override
+ public void add(byte[] bytes) {
+ messageDigest.update(bytes);
+ }
+
+ public void update(Prefix prefix) {
+ messageDigest.update(prefix.bytes());
+ }
+
+ public HalfSha512 add(SerializedType st) {
+ st.toBytesSink(this);
+ return this;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash.java
new file mode 100644
index 0000000000..898d1b2913
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash.java
@@ -0,0 +1,155 @@
+package com.ripple.core.coretypes.hash;
+
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+
+abstract public class Hash implements SerializedType, Comparable {
+ protected final byte[] hash;
+
+ public Hash(byte[] bytes, int size) {
+ hash = checkHash(bytes, size);
+ }
+
+ @Override
+ public String toString() {
+ return B16.encode(hash);
+ }
+
+ @Override
+ public int hashCode() {
+ // Having this lazily computed and then cached would give significant
+ // performance boosts in some single threaded contexts. Given the inputs
+ // for the method are at least effectively final, and the method is
+ // essentially a pure function, it seems probably safe to do. It
+ // seems at worst, that in a multi threaded context, some extra work
+ // will be done in rare cases?
+ // TODO: check this assumption
+ return Arrays.hashCode(hash);
+ }
+
+ private byte[] checkHash(byte[] bytes, int size) {
+ int length = bytes.length;
+ if (length > size) {
+ throwIllegalArg(length, "wide");
+ } else if (length == size) {
+ // TODO: What costs for this ?
+ return bytes.clone();
+ } else {
+ throwIllegalArg(length, "small");
+ }
+ throw new IllegalStateException("Can not get here");
+ }
+
+ private void throwIllegalArg(int length, String wrongness) {
+ String simpleName = getClass().getSimpleName();
+ throw new IllegalArgumentException("Hash length of " + length + "bytes is too " +
+ wrongness +
+ " for " + simpleName);
+ }
+
+ BigInteger bigInteger() {
+ return new BigInteger(1, hash);
+ }
+
+ // TODO: Is a defensive copy worthwhile? Or just be adults? Never been a
+ // problem in practice, though others may not know to avoid mutating the
+ // returned array. Pity Java doesn't have something like C++ const.
+ // If you did it in one place, should do it EVERY WHERE, inputs/outputs and
+ // on every type that stores mutable arrays internally and the perf and
+ // maybe memory (at least GC stress) costs could add up.
+ // It's a bit like polishing a tur*.
+ public byte[] bytes() {
+ return hash.clone();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Hash) {
+ return Arrays.equals(hash, ((Hash) obj).hash);
+ }
+
+ return super.equals(obj);
+ }
+
+ @Override
+ public int compareTo(Subclass another) {
+ byte[] thisBytes = bytes();
+ byte[] bytes = another.bytes();
+
+ return compareBytes(thisBytes, bytes, 0, thisBytes.length);
+ }
+
+ public int compareStartingAt(Subclass another, int start) {
+ byte[] thisBytes = bytes();
+ byte[] bytes = another.bytes();
+
+ return compareBytes(thisBytes, bytes, start, thisBytes.length);
+ }
+
+ private int compareBytes(byte[] thisBytes, byte[] bytes, int start, int numBytes) {
+ int thisLength = thisBytes.length;
+ if (!(bytes.length == thisLength)) {
+ throw new RuntimeException();
+ }
+
+ for (int i = start; i < numBytes; i++) {
+ int cmp = (thisBytes[i] & 0xFF) - (bytes[i] & 0xFF);
+ if (cmp != 0) {
+ return cmp < 0 ? -1 : 1;
+ }
+ }
+ return 0;
+ }
+
+ public byte[] slice(int start) {
+ return slice(start, 0);
+ }
+
+ public byte get(int i) {
+ if (i < 0) i += hash.length;
+ return hash[i];
+ }
+
+ private byte[] slice(int start, int end) {
+ if (start < 0) start += hash.length;
+ if (end <= 0) end += hash.length;
+
+ int length = end - start;
+ byte[] slice = new byte[length];
+
+ System.arraycopy(hash, start, slice, 0, length);
+ return slice;
+ }
+
+ static public abstract class HashTranslator extends TypeTranslator {
+
+ public abstract T newInstance(byte[] b);
+ public abstract int byteWidth();
+
+ @Override
+ public T fromParser(BinaryParser parser, Integer hint) {
+ return newInstance(parser.read(byteWidth()));
+ }
+
+ @Override
+ public Object toJSON(T obj) {
+ return B16.encode(obj.hash);
+ }
+
+ @Override
+ public T fromString(String value) {
+ return newInstance(B16.decode(value));
+ }
+
+ @Override
+ public void toBytesSink(T obj, BytesSink to) {
+ to.add(obj.bytes());
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash128.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash128.java
new file mode 100644
index 0000000000..112340da9f
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash128.java
@@ -0,0 +1,70 @@
+package com.ripple.core.coretypes.hash;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Hash128Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+
+public class Hash128 extends Hash {
+ public Hash128(byte[] bytes) {
+ super(bytes, 16);
+ }
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.Hash128;
+ }
+
+ public static Hash128 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ public static Hash128 fromHex(String string) {
+ return translate.fromHex(string);
+ }
+ public static Hash128 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ public static class Translator extends HashTranslator {
+ @Override
+ public Hash128 newInstance(byte[] b) {
+ return new Hash128(b);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 16;
+ }
+ }
+
+ public static Translator translate = new Translator();
+
+ private static Hash128Field hash128Field(final Field f) {
+ return new Hash128Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public Hash128Field EmailHash = hash128Field(Field.EmailHash);
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash160.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash160.java
new file mode 100644
index 0000000000..5f237c195c
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash160.java
@@ -0,0 +1,81 @@
+package com.ripple.core.coretypes.hash;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Hash160Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+
+public class Hash160 extends Hash {
+ public Hash160(byte[] bytes) {
+ super(bytes, 20);
+ }
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.Hash160;
+ }
+
+ public static Hash160 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ public static Hash160 fromHex(String string) {
+ return translate.fromHex(string);
+ }
+
+ public static Hash160 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ public static class Translator extends HashTranslator {
+ @Override
+ public Hash160 newInstance(byte[] b) {
+ return new Hash160(b);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 20;
+ }
+
+ @Override
+ public Hash160 fromString(String value) {
+ if (value.startsWith("r")) {
+ return newInstance(AccountID.fromAddress(value).bytes());
+ }
+ return super.fromString(value);
+ }
+ }
+ public static Translator translate = new Translator();
+
+ private static Hash160Field hash160Field(final Field f) {
+ return new Hash160Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public Hash160Field TakerPaysIssuer = hash160Field(Field.TakerPaysIssuer);
+ static public Hash160Field TakerGetsCurrency = hash160Field(Field.TakerGetsCurrency);
+ static public Hash160Field TakerPaysCurrency = hash160Field(Field.TakerPaysCurrency);
+ static public Hash160Field TakerGetsIssuer = hash160Field(Field.TakerGetsIssuer);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash256.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash256.java
new file mode 100644
index 0000000000..678b15ef00
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Hash256.java
@@ -0,0 +1,146 @@
+package com.ripple.core.coretypes.hash;
+
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Hash256Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+
+import java.math.BigInteger;
+import java.util.TreeMap;
+
+public class Hash256 extends Hash {
+
+ public static final BigInteger bookBaseSize = new BigInteger("10000000000000000", 16);
+
+ public int divergenceDepth(Hash256 other) {
+ return divergenceDepth(0, other);
+ }
+ public int divergenceDepth(int i, Hash256 other) {
+ for (; i < 64; i++) {
+ if (nibblet(i) != other.nibblet(i)) {
+ break;
+ }
+ }
+ return i;
+ }
+
+ public static class Hash256Map extends TreeMap {
+ public Hash256Map(Hash256Map cache) {
+ super(cache);
+ }
+ public Hash256Map() {
+
+ }
+ }
+ public static final Hash256 ZERO_256 = new Hash256(new byte[32]);
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.Hash256;
+ }
+
+ public boolean isZero() {
+ return this == Hash256.ZERO_256 || equals(Hash256.ZERO_256);
+ }
+
+ public boolean isNonZero() {
+ return !isZero();
+ }
+
+ public static Hash256 fromHex(String s) {
+ return translate.fromHex(s);
+ }
+ public static Hash256 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+ public static Hash256 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ public Hash256(byte[] bytes) {
+ super(bytes, 32);
+ }
+
+ public static Hash256 signingHash(byte[] blob) {
+ return prefixedHalfSha512(HashPrefix.txSign, blob);
+ }
+
+ public static Hash256 prefixedHalfSha512(Prefix prefix, byte[] blob) {
+ HalfSha512 messageDigest = HalfSha512.prefixed256(prefix);
+ messageDigest.update(blob);
+ return messageDigest.finish();
+ }
+
+ public int nibblet(int depth) {
+ int byte_ix = depth > 0 ? depth / 2 : 0;
+ int b = super.hash[byte_ix];
+ if (depth % 2 == 0) {
+ b = (b & 0xF0) >> 4;
+ } else {
+ b = b & 0x0F;
+ }
+ return b;
+ }
+
+ public static class Translator extends HashTranslator {
+ @Override
+ public Hash256 newInstance(byte[] b) {
+ return new Hash256(b);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 32;
+ }
+ }
+ public static Translator translate = new Translator();
+
+ public static Hash256Field hash256Field(final Field f) {
+ return new Hash256Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public Hash256Field LedgerHash = hash256Field(Field.LedgerHash);
+ static public Hash256Field ParentHash = hash256Field(Field.ParentHash);
+ static public Hash256Field TransactionHash = hash256Field(Field.TransactionHash);
+ static public Hash256Field AccountHash = hash256Field(Field.AccountHash);
+ static public Hash256Field PreviousTxnID = hash256Field(Field.PreviousTxnID);
+ static public Hash256Field AccountTxnID = hash256Field(Field.AccountTxnID);
+ static public Hash256Field LedgerIndex = hash256Field(Field.LedgerIndex);
+ static public Hash256Field WalletLocator = hash256Field(Field.WalletLocator);
+ static public Hash256Field RootIndex = hash256Field(Field.RootIndex);
+ static public Hash256Field BookDirectory = hash256Field(Field.BookDirectory);
+ static public Hash256Field InvoiceID = hash256Field(Field.InvoiceID);
+ static public Hash256Field Nickname = hash256Field(Field.Nickname);
+ static public Hash256Field Amendment = hash256Field(Field.Amendment);
+ static public Hash256Field TicketID = hash256Field(Field.TicketID);
+ static public Hash256Field Channel = hash256Field(Field.Channel);
+ static public Hash256Field CheckID = hash256Field(Field.CheckID);
+ static public Hash256Field Digest = hash256Field(Field.Digest);
+ static public Hash256Field ConsensusHash = hash256Field(Field.ConsensusHash);
+
+ static public Hash256Field hash = hash256Field(Field.hash);
+ static public Hash256Field index = hash256Field(Field.index);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Index.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Index.java
new file mode 100644
index 0000000000..75a5900430
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/Index.java
@@ -0,0 +1,159 @@
+package com.ripple.core.coretypes.hash;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Currency;
+import com.ripple.core.coretypes.Issue;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.hash.prefixes.LedgerSpace;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.ripple.core.coretypes.hash.HalfSha512.prefixed256;
+import static java.util.Collections.sort;
+
+public class Index {
+ private static Hash256 createBookBase(Issue pays, Issue gets) {
+ return prefixed256(LedgerSpace.bookDir)
+ .add(pays.currency())
+ .add(gets.currency())
+ .add(pays.issuer())
+ .add(gets.issuer())
+ .finish();
+ }
+
+ /**
+ *
+ * @return a copy of index, with quality overlaid in lowest 8 bytes
+ */
+ public static Hash256 quality(Hash256 index, UInt64 quality) {
+ byte[] qi = new byte[32];
+ System.arraycopy(index.bytes(), 0, qi, 0, 24);
+ if (quality != null) System.arraycopy(quality.toBytes(), 0, qi, 24, 8);
+ return new Hash256(qi);
+ }
+
+ /**
+ * @return A copy of index, with the lowest 8 bytes all zeroed.
+ */
+ private static Hash256 zeroQuality(Hash256 fullIndex) {
+ return quality(fullIndex, null);
+ }
+
+ public static Hash256 rippleState(AccountID a1, AccountID a2, Currency currency) {
+ List accounts = Arrays.asList(a1, a2);
+ sort(accounts);
+ return rippleState(accounts, currency);
+ }
+
+ public static Hash256 rippleState(List sortedAccounts, Currency currency) {
+ HalfSha512 hasher = prefixed256(LedgerSpace.ripple);
+ // Low then High
+ for (AccountID account : sortedAccounts) account.toBytesSink(hasher);
+ // Currency
+ currency.toBytesSink(hasher);
+
+ return hasher.finish();
+ }
+
+ /**
+ *
+ * @param rootIndex The RootIndex index for the directory node
+ * @param nodeIndex nullable LowNode, HighNode, OwnerNode, BookNode etc
+ * defining a `page` number.
+ *
+ * @return A hash of rootIndex and nodeIndex when nodeIndex is non default
+ * else the rootIndex. This hash is used as an index for the next
+ * DirectoryNode page.
+ */
+ public static Hash256 directoryNode(Hash256 rootIndex, UInt64 nodeIndex) {
+ if (nodeIndex == null || nodeIndex.isZero()) {
+ return rootIndex;
+ }
+
+ return prefixed256(LedgerSpace.dirNode)
+ .add(rootIndex)
+ .add(nodeIndex)
+ .finish();
+ }
+
+ public static Hash256 accountRoot(AccountID accountID) {
+ return prefixed256(LedgerSpace.account).add(accountID).finish();
+ }
+
+ public static Hash256 paymentChannel(AccountID account, AccountID destination, UInt32 sequence) {
+ return prefixed256(LedgerSpace.paymentChannel)
+ .add(account)
+ .add(destination)
+ .add(sequence)
+ .finish();
+ }
+
+ public static Hash256 escrow(AccountID account, UInt32 sequence) {
+ return prefixed256(LedgerSpace.escrow)
+ .add(account)
+ .add(sequence)
+ .finish();
+ }
+
+ public static Hash256 ownerDirectory(AccountID account) {
+ return Hash256.prefixedHalfSha512(LedgerSpace.ownerDir, account.bytes());
+ }
+
+ public static Hash256 transactionID(byte[] blob) {
+ return Hash256.prefixedHalfSha512(HashPrefix.transactionID, blob);
+ }
+
+ public static Hash256 bookStart(Issue pays, Issue gets) {
+ return zeroQuality(createBookBase(pays, gets));
+ }
+
+ public static Hash256 bookStart(Hash256 indexFromBookRange) {
+ return zeroQuality(indexFromBookRange);
+ }
+
+ public static Hash256 bookEnd(Hash256 base) {
+ byte[] end = base.bigInteger().add(Hash256.bookBaseSize).toByteArray();
+ if (end.length > 32) {
+ byte[] source = end;
+ end = new byte[32];
+ System.arraycopy(source, source.length - 32, end, 0, 32);
+ }
+ return new Hash256(end);
+ }
+
+ public static Hash256 ledgerHashes(long prev) {
+ return prefixed256(LedgerSpace.skipList)
+ .add(new UInt32(prev >> 16))
+ .finish();
+ }
+ public static Hash256 ledgerHashes() {
+ return prefixed256(LedgerSpace.skipList).finish();
+ }
+
+ public static Hash256 amendments() {
+ return prefixed256(LedgerSpace.amendment).finish();
+ }
+
+ public static Hash256 feeSettings() {
+ return prefixed256(LedgerSpace.fee).finish();
+ }
+
+ public static Hash256 signerList(AccountID account) {
+ return prefixed256(LedgerSpace.signerList)
+ .add(account)
+ .add(UInt32.ZERO)
+ .finish();
+ }
+
+ public static Hash256 ticket(AccountID account, UInt32 sequence) {
+ return prefixed256(LedgerSpace.ticket)
+ .add(account)
+ .add(sequence)
+ .finish();
+ }
+
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/HashPrefix.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/HashPrefix.java
new file mode 100644
index 0000000000..f213d4eb38
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/HashPrefix.java
@@ -0,0 +1,56 @@
+package com.ripple.core.coretypes.hash.prefixes;
+
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.encodings.common.B16;
+
+/**
+ * The prefix codes are part of the Ripple protocol
+ * and existing codes cannot be arbitrarily changed.
+ */
+public enum HashPrefix implements Prefix {
+ transactionID ('T', 'X', 'N'),
+ txNode ('S', 'N', 'D'),
+ leafNode ('M', 'L', 'N'),
+ innerNode ('M', 'I', 'N'),
+ innerNodeV2 ('I', 'N', 'R'),
+ ledgerMaster ('L', 'W', 'R'),
+ txSign ('S', 'T', 'X'),
+ txMultiSign ('S', 'M', 'T'),
+ validation ('V', 'A', 'L'),
+ proposal ('P', 'R', 'P'),
+ manifest ('M', 'A', 'N'),
+ paymentChannelClaim ('C', 'L', 'M');
+
+ private UInt32 uInt32;
+ private byte[] bytes;
+ private String chars;
+
+ @Override
+ public byte[] bytes() {
+ return bytes;
+ }
+
+ public String toHex() {
+ return B16.encode(bytes);
+ }
+
+ HashPrefix(char... chars) {
+ this.chars = new String(chars);
+ byte[] bytes = {
+ (byte) chars[0],
+ (byte) chars[1],
+ (byte) chars[2],
+ (byte) 0,
+ };
+ uInt32 = UInt32.fromBytes(bytes);
+ this.bytes = bytes;
+ }
+
+ public UInt32 uInt32() {
+ return uInt32;
+ }
+
+ public String chars() {
+ return chars;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/LedgerSpace.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/LedgerSpace.java
new file mode 100644
index 0000000000..20f5d61388
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/LedgerSpace.java
@@ -0,0 +1,38 @@
+package com.ripple.core.coretypes.hash.prefixes;
+
+import com.ripple.core.coretypes.uint.UInt16;
+
+public enum LedgerSpace implements Prefix {
+ account('a'),
+ dirNode('d'),
+ generator('g'),
+
+ ripple('r'),
+ offer('o'), // Entry for an offer.
+ ownerDir('O'), // Directory of things owned by an account.
+ bookDir('B'), // Directory of order books.
+ contract('c'),
+ skipList('s'),
+ escrow('u'),
+ amendment('f'),
+ fee('e'),
+ ticket('T'),
+ signerList('S'),
+ paymentChannel('x'),
+
+ // no longer used
+ nickname('n'),;
+
+ UInt16 uInt16;
+ public byte[] bytes;
+
+ @Override
+ public byte[] bytes() {
+ return bytes;
+ }
+
+ LedgerSpace(char c) {
+ uInt16 = new UInt16((int) c);
+ bytes = uInt16.toByteArray();
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/Prefix.java b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/Prefix.java
new file mode 100644
index 0000000000..7ba8b7a62a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/hash/prefixes/Prefix.java
@@ -0,0 +1,5 @@
+package com.ripple.core.coretypes.hash.prefixes;
+
+public interface Prefix {
+ byte[] bytes();
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt.java
new file mode 100644
index 0000000000..aae0dcedf3
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt.java
@@ -0,0 +1,209 @@
+package com.ripple.core.coretypes.uint;
+
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+import com.ripple.utils.Utils;
+
+import java.math.BigInteger;
+
+abstract public class UInt extends Number implements SerializedType, Comparable {
+ private final BigInteger value;
+
+ private static BigInteger[] upperBounds = new BigInteger[8];
+ private static BigInteger maxUIntVal(int bits) {
+ return new BigInteger("2").pow(bits).subtract(BigInteger.ONE);
+ }
+
+ static {
+ for (int i = 0; i < 8; i++) {
+ upperBounds[i] = maxUIntVal((i + 1) * 8 );
+ }
+ }
+
+ UInt(byte[] bytes) {
+ value = new BigInteger(1, bytes);
+ checkBounds();
+ }
+
+ private void checkBounds() {
+ BigInteger upper = upperBounds[getByteWidth() - 1];
+ if (value.compareTo(upper) > 0 || value.compareTo(BigInteger.ZERO) < 0) {
+ throw new IllegalArgumentException("value `" + value +
+ "` is illegal for " + getClass().getSimpleName());
+ }
+ }
+
+ UInt(BigInteger bi) {
+ value = (bi);
+ checkBounds();
+ }
+ UInt(Number s) {
+ value = (BigInteger.valueOf(s.longValue()));
+ checkBounds();
+ }
+ UInt(String s) {
+ value = (new BigInteger(s));
+ checkBounds();
+ }
+ UInt(String s, int radix) {
+ value = (new BigInteger(s, radix));
+ checkBounds();
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+
+ public abstract int getByteWidth();
+ protected abstract Subclass instanceFrom(BigInteger n);
+
+ public Subclass add(UInt val) {
+ return instanceFrom(value.add(val.value));
+ }
+
+ public Subclass subtract(UInt val) {
+ return instanceFrom(value.subtract(val.value));
+ }
+
+ public Subclass multiply(UInt val) {
+ return instanceFrom(value.multiply(val.value));
+ }
+
+ public Subclass divide(UInt val) {
+ return instanceFrom(value.divide(val.value));
+ }
+
+ public Subclass or(UInt val) {
+ return instanceFrom(value.or(val.value));
+ }
+
+ public Subclass shiftLeft(int n) {
+ return instanceFrom(value.shiftLeft(n));
+ }
+
+ public Subclass shiftRight(int n) {
+ return instanceFrom(value.shiftRight(n));
+ }
+
+ public int compareTo(UInt val) {
+ return value.compareTo(val.value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof UInt) {
+ return equals((UInt) obj);
+ }
+ else return super.equals(obj);
+ }
+
+ public boolean equals(UInt x) {
+ return value.equals(x.value);
+ }
+
+ public String toString(int radix) {
+ return value.toString(radix);
+ }
+ public byte[] toByteArray() {
+ int length = getByteWidth();
+ return Utils.leadingZeroesTrimmedOrPaddedTo(length, value.toByteArray());
+ }
+
+ abstract public Object value();
+
+ public BigInteger bigInteger(){
+ return value;
+ }
+
+ @Override
+ public int intValue() {
+ return value.intValue();
+ }
+
+ @Override
+ public long longValue() {
+ return value.longValue();
+ }
+
+ @Override
+ public double doubleValue() {
+ return value.doubleValue();
+ }
+
+ @Override
+ public float floatValue() {
+ return value.floatValue();
+ }
+
+ @Override
+ public byte byteValue() {
+ return value.byteValue();
+ }
+
+ @Override
+ public short shortValue() {
+ return value.shortValue();
+ }
+
+ public boolean lte(T sequence) {
+ return compareTo(sequence) < 1;
+ }
+
+ public boolean testBit(int f) {
+ // TODO, optimized ;) // move to Uint32
+ return value.testBit(f);
+ }
+
+ public boolean isZero() {
+ return value.signum() == 0;
+ }
+
+ static public abstract class UINTTranslator extends TypeTranslator {
+ public abstract T newInstance(BigInteger i);
+ public abstract int byteWidth();
+
+ @Override
+ public T fromParser(BinaryParser parser, Integer hint) {
+ return newInstance(new BigInteger(1, parser.read(byteWidth())));
+ }
+
+ @Override
+ public Object toJSON(T obj) {
+ if (obj.getByteWidth() <= 4) {
+ return obj.longValue();
+ } else {
+ return toString(obj);
+ }
+ }
+
+ @Override
+ public T fromLong(long aLong) {
+ return newInstance(BigInteger.valueOf(aLong));
+ }
+
+ @Override
+ public T fromString(String value) {
+ int radix = byteWidth() <= 4 ? 10 : 16;
+ return newInstance(new BigInteger(value, radix));
+ }
+
+ @Override
+ public T fromInteger(int integer) {
+ return fromLong(integer);
+ }
+
+ @Override
+ public String toString(T obj) {
+ return B16.encode(obj.toByteArray());
+ }
+
+ @Override
+ public void toBytesSink(T obj, BytesSink to) {
+ to.add(obj.toByteArray());
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt16.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt16.java
new file mode 100644
index 0000000000..9634cf6082
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt16.java
@@ -0,0 +1,106 @@
+package com.ripple.core.coretypes.uint;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.fields.UInt16Field;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.TypeTranslator;
+
+import java.math.BigInteger;
+
+public class UInt16 extends UInt {
+ public final static UInt16 ZERO = new UInt16(0);
+
+ public static TypeTranslator translate = new UINTTranslator() {
+ @Override
+ public UInt16 newInstance(BigInteger i) {
+ return new UInt16(i);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 2;
+ }
+ };
+
+ public UInt16(byte[] bytes) {
+ super(bytes);
+ }
+
+ public UInt16(BigInteger value) {
+ super(value);
+ }
+
+ public UInt16(Number s) {
+ super(s);
+ }
+
+ public UInt16(String s) {
+ super(s);
+ }
+
+ public UInt16(String s, int radix) {
+ super(s, radix);
+ }
+
+ public static UInt16 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ public static UInt16 fromHex(String string) {
+ return translate.fromHex(string);
+ }
+
+ public static UInt16 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ @Override
+ public int getByteWidth() {
+ return 2;
+ }
+
+ @Override
+ protected UInt16 instanceFrom(BigInteger n) {
+ return new UInt16(n);
+ }
+
+ @Override
+ public Integer value() {
+ return intValue();
+ }
+
+ private static UInt16Field int16Field(final Field f) {
+ return new UInt16Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public UInt16Field LedgerEntryType = int16Field(Field.LedgerEntryType);
+ static public UInt16Field TransactionType = int16Field(Field.TransactionType);
+ static public UInt16Field SignerWeight = int16Field(Field.SignerWeight);
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt16;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt32.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt32.java
new file mode 100644
index 0000000000..b831c8607c
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt32.java
@@ -0,0 +1,137 @@
+package com.ripple.core.coretypes.uint;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.fields.UInt32Field;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.TypeTranslator;
+
+import java.math.BigInteger;
+
+public class UInt32 extends UInt {
+ public final static UInt32 ZERO = new UInt32(0);
+
+ public static TypeTranslator translate = new UINTTranslator() {
+ @Override
+ public UInt32 newInstance(BigInteger i) {
+ return new UInt32(i);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 4;
+ }
+ };
+
+ public UInt32(byte[] bytes) {
+ super(bytes);
+ }
+
+ public UInt32(BigInteger value) {
+ super(value);
+ }
+
+ public UInt32(Number val) {
+ super(val);
+ }
+ public UInt32(String s) {
+ super(s);
+ }
+
+ public UInt32(String s, int radix) {
+ super(s, radix);
+ }
+
+ public static UInt32 fromParser(BinaryParser binaryParser) {
+ return translate.fromParser(binaryParser);
+ }
+ public static UInt32 fromHex(String string) {
+ return translate.fromHex(string);
+ }
+ public static UInt32 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ @Override
+ public int getByteWidth() {
+ return 4;
+ }
+
+ @Override
+ protected UInt32 instanceFrom(BigInteger n) {
+ return new UInt32(n);
+ }
+
+ @Override
+ public Long value() {
+ return longValue();
+ }
+
+ private static UInt32Field int32Field(final Field f) {
+ return new UInt32Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public UInt32Field Flags = int32Field(Field.Flags);
+ static public UInt32Field SourceTag = int32Field(Field.SourceTag);
+ static public UInt32Field Sequence = int32Field(Field.Sequence);
+ static public UInt32Field PreviousTxnLgrSeq = int32Field(Field.PreviousTxnLgrSeq);
+ static public UInt32Field LedgerSequence = int32Field(Field.LedgerSequence);
+ static public UInt32Field CloseTime = int32Field(Field.CloseTime);
+ static public UInt32Field ParentCloseTime = int32Field(Field.ParentCloseTime);
+ static public UInt32Field SigningTime = int32Field(Field.SigningTime);
+ static public UInt32Field Expiration = int32Field(Field.Expiration);
+ static public UInt32Field TransferRate = int32Field(Field.TransferRate);
+ static public UInt32Field WalletSize = int32Field(Field.WalletSize);
+ static public UInt32Field OwnerCount = int32Field(Field.OwnerCount);
+ static public UInt32Field DestinationTag = int32Field(Field.DestinationTag);
+ static public UInt32Field HighQualityIn = int32Field(Field.HighQualityIn);
+ static public UInt32Field HighQualityOut = int32Field(Field.HighQualityOut);
+ static public UInt32Field LowQualityIn = int32Field(Field.LowQualityIn);
+ static public UInt32Field LowQualityOut = int32Field(Field.LowQualityOut);
+ static public UInt32Field QualityIn = int32Field(Field.QualityIn);
+ static public UInt32Field QualityOut = int32Field(Field.QualityOut);
+ static public UInt32Field StampEscrow = int32Field(Field.StampEscrow);
+ static public UInt32Field BondAmount = int32Field(Field.BondAmount);
+ static public UInt32Field LoadFee = int32Field(Field.LoadFee);
+ static public UInt32Field OfferSequence = int32Field(Field.OfferSequence);
+ static public UInt32Field FirstLedgerSequence = int32Field(Field.FirstLedgerSequence);
+ static public UInt32Field LastLedgerSequence = int32Field(Field.LastLedgerSequence);
+ static public UInt32Field TransactionIndex = int32Field(Field.TransactionIndex);
+ static public UInt32Field OperationLimit = int32Field(Field.OperationLimit);
+ static public UInt32Field ReferenceFeeUnits = int32Field(Field.ReferenceFeeUnits);
+ static public UInt32Field ReserveBase = int32Field(Field.ReserveBase);
+ static public UInt32Field ReserveIncrement = int32Field(Field.ReserveIncrement);
+ static public UInt32Field SetFlag = int32Field(Field.SetFlag);
+ static public UInt32Field ClearFlag = int32Field(Field.ClearFlag);
+ static public UInt32Field SignerQuorum = int32Field(Field.SignerQuorum);
+ public static UInt32Field SignerListID = int32Field(Field.SignerListID);
+ public static UInt32Field CancelAfter = int32Field(Field.CancelAfter);
+ public static UInt32Field FinishAfter = int32Field(Field.FinishAfter);
+ public static UInt32Field SettleDelay = int32Field(Field.SettleDelay);
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt32;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt64.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt64.java
new file mode 100644
index 0000000000..3312192542
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt64.java
@@ -0,0 +1,112 @@
+package com.ripple.core.coretypes.uint;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.fields.UInt64Field;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.TypeTranslator;
+
+import java.math.BigInteger;
+
+public class UInt64 extends UInt {
+ public final static UInt64 ZERO = new UInt64(0);
+
+ public static TypeTranslator translate = new UINTTranslator() {
+ @Override
+ public UInt64 newInstance(BigInteger i) {
+ return new UInt64(i);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 8;
+ }
+ };
+
+ public UInt64(byte[] bytes) {
+ super(bytes);
+ }
+
+ public UInt64(BigInteger value) {
+ super(value);
+ }
+
+ public UInt64(Number s) {
+ super(s);
+ }
+
+ public UInt64(String s) {
+ super(s);
+ }
+
+ public UInt64(String s, int radix) {
+ super(s, radix);
+ }
+
+ public static UInt64 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+
+ public static UInt64 fromHex(String string) {
+ return translate.fromHex(string);
+ }
+ public static UInt64 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ @Override
+ public int getByteWidth() {
+ return 8;
+ }
+
+ @Override
+ protected UInt64 instanceFrom(BigInteger n) {
+ return new UInt64(n);
+ }
+
+ @Override
+ public BigInteger value() {
+ return bigInteger();
+ }
+
+ private static UInt64Field int64Field(final Field f) {
+ return new UInt64Field(){ @Override public Field getField() {return f;}};
+ }
+
+ static public UInt64Field IndexNext = int64Field(Field.IndexNext);
+ static public UInt64Field IndexPrevious = int64Field(Field.IndexPrevious);
+ static public UInt64Field BookNode = int64Field(Field.BookNode);
+ static public UInt64Field OwnerNode = int64Field(Field.OwnerNode);
+ static public UInt64Field BaseFee = int64Field(Field.BaseFee);
+ static public UInt64Field ExchangeRate = int64Field(Field.ExchangeRate);
+ static public UInt64Field LowNode = int64Field(Field.LowNode);
+ static public UInt64Field HighNode = int64Field(Field.HighNode);
+ public static UInt64Field DestinationNode = int64Field(Field.DestinationNode);
+ public static UInt64Field Cookie = int64Field(Field.Cookie);
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt64;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt8.java b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt8.java
new file mode 100644
index 0000000000..528eaa8614
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/coretypes/uint/UInt8.java
@@ -0,0 +1,105 @@
+package com.ripple.core.coretypes.uint;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+import com.ripple.core.fields.UInt8Field;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.TypeTranslator;
+
+import java.math.BigInteger;
+
+public class UInt8 extends UInt {
+ public final static UInt8 ZERO = new UInt8(0);
+
+ public static TypeTranslator translate = new UINTTranslator() {
+ @Override
+ public UInt8 newInstance(BigInteger i) {
+ return new UInt8(i);
+ }
+
+ @Override
+ public int byteWidth() {
+ return 1;
+ }
+ };
+
+ public UInt8(byte[] bytes) {
+ super(bytes);
+ }
+
+ public UInt8(BigInteger value) {
+ super(value);
+ }
+
+ public UInt8(Number s) {
+ super(s);
+ }
+
+ public UInt8(String s) {
+ super(s);
+ }
+
+ public UInt8(String s, int radix) {
+ super(s, radix);
+ }
+
+ public static UInt8 fromParser(BinaryParser parser) {
+ return translate.fromParser(parser);
+ }
+ public static UInt8 fromHex(String hex) {
+ return translate.fromHex(hex);
+ }
+ public static UInt8 fromBytes(byte[] bytes) {
+ return translate.fromBytes(bytes);
+ }
+
+ @Override
+ public int getByteWidth() {
+ return 1;
+ }
+
+ @Override
+ protected UInt8 instanceFrom(BigInteger n) {
+ return new UInt8(n);
+ }
+
+ @Override
+ public Short value() {
+ return shortValue();
+ }
+
+ private static UInt8Field int8Field(final Field f) {
+ return new UInt8Field() {@Override public Field getField() {return f; } };
+ }
+
+ static public UInt8Field CloseResolution = int8Field(Field.CloseResolution);
+ static public UInt8Field Method = int8Field(Field.Method);
+ static public UInt8Field TransactionResult = int8Field(Field.TransactionResult);
+ static public UInt8Field TickSize = int8Field(Field.TickSize);
+
+ @Override
+ public Object toJSON() {
+ return translate.toJSON(this);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ return translate.toBytes(this);
+ }
+
+ @Override
+ public String toHex() {
+ return translate.toHex(this);
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ translate.toBytesSink(this, to);
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt8;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/enums/LedgerFlag.java b/ripple-core/src/main/java/com/ripple/core/enums/LedgerFlag.java
new file mode 100644
index 0000000000..99f18b5309
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/enums/LedgerFlag.java
@@ -0,0 +1,29 @@
+package com.ripple.core.enums;
+
+// Ledger Specific Flags
+public class LedgerFlag {
+ public static int
+ // ltACCOUNT_ROOT
+ PasswordSpent = 0x00010000, // True, if password set fee is spent.
+ RequireDestTag = 0x00020000, // True, to require a DestinationTag for payments.
+ RequireAuth = 0x00040000, // True, to require a authorization to hold IOUs.
+ DisallowXRP = 0x00080000, // True, to disallow sending XRP.
+ DisableMaster = 0x00100000, // True, force regular key
+ NoFreeze = 0x00200000, // True, cannot freeze ripple states
+ GlobalFreeze = 0x00400000, // True, all assets frozen
+ DefaultRipple = 0x00800000, // True, all assets frozen
+
+ // ltOFFER
+ Passive = 0x00010000,
+ Sell = 0x00020000, // True, offer was placed as a sell.
+
+ // ltRIPPLE_STATE
+ LowReserve = 0x00010000, // True, if entry counts toward reserve.
+ HighReserve = 0x00020000,
+ LowAuth = 0x00040000,
+ HighAuth = 0x00080000,
+ LowNoRipple = 0x00100000,
+ HighNoRipple = 0x00200000,
+ LowFreeze = 0x00400000, // True, low side has set freeze flag
+ HighFreeze = 0x00800000; // True, high side has set freeze flag
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/enums/TransactionFlag.java b/ripple-core/src/main/java/com/ripple/core/enums/TransactionFlag.java
new file mode 100644
index 0000000000..adb7f9e910
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/enums/TransactionFlag.java
@@ -0,0 +1,50 @@
+package com.ripple.core.enums;
+
+// Transaction Specific Flags
+public class TransactionFlag {
+ public static long
+ FullyCanonicalSig = 0x80000000L,
+ Universal = FullyCanonicalSig,
+ UniversalMask = ~Universal,
+
+ // AccountSet flags:
+ RequireDestTag = 0x00010000,
+ OptionalDestTag = 0x00020000,
+ RequireAuth = 0x00040000,
+ OptionalAuth = 0x00080000,
+ DisallowXRP = 0x00100000,
+ AllowXRP = 0x00200000,
+ AccountSetMask = ~(Universal | RequireDestTag | OptionalDestTag
+ | RequireAuth | OptionalAuth
+ | DisallowXRP | AllowXRP),
+
+ // AccountSet SetFlag/ClearFlag values
+ asfRequireDest = 1,
+ asfRequireAuth = 2,
+ asfDisallowXRP = 3,
+ asfDisableMaster = 4,
+ asfAccountTxnID = 5,
+ asfNoFreeze = 6,
+ asfGlobalFreeze = 7,
+
+ // OfferCreate flags:
+ Passive = 0x00010000,
+ ImmediateOrCancel = 0x00020000,
+ FillOrKill = 0x00040000,
+ Sell = 0x00080000,
+ OfferCreateMask = ~(Universal | Passive | ImmediateOrCancel | FillOrKill | Sell),
+
+ // Payment flags:
+ NoRippleDirect = 0x00010000,
+ PartialPayment = 0x00020000,
+ LimitQuality = 0x00040000,
+ PaymentMask = ~(Universal | PartialPayment | LimitQuality | NoRippleDirect),
+
+ // TrustSet flags:
+ SetAuth = 0x00010000,
+ SetNoRipple = 0x00020000,
+ ClearNoRipple = 0x00040000,
+ SetFreeze = 0x00100000,
+ ClearFreeze = 0x00200000,
+ TrustSetMask = ~(Universal | SetAuth | SetNoRipple | ClearNoRipple | SetFreeze | ClearFreeze);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/AccountIDField.java b/ripple-core/src/main/java/com/ripple/core/fields/AccountIDField.java
new file mode 100644
index 0000000000..f31e3df14d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/AccountIDField.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class AccountIDField implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/AmountField.java b/ripple-core/src/main/java/com/ripple/core/fields/AmountField.java
new file mode 100644
index 0000000000..4666ff51f9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/AmountField.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class AmountField implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/BlobField.java b/ripple-core/src/main/java/com/ripple/core/fields/BlobField.java
new file mode 100644
index 0000000000..2925c89611
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/BlobField.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class BlobField implements HasField{}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Field.java
new file mode 100644
index 0000000000..87871ecca0
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/Field.java
@@ -0,0 +1,328 @@
+package com.ripple.core.fields;
+
+import java.util.*;
+
+public enum Field {
+ // These are all presorted (verified in a static block below)
+ // They can then be used in a TreeMap, using the Enum (private) ordinal
+ // comparator
+ Generic(0, Type.Unknown),
+ Invalid(-1, Type.Unknown),
+
+ LedgerEntryType(1, Type.UInt16),
+ TransactionType(2, Type.UInt16),
+ SignerWeight(3, Type.UInt16),
+
+ Flags(2, Type.UInt32),
+ SourceTag(3, Type.UInt32),
+ Sequence(4, Type.UInt32),
+ PreviousTxnLgrSeq(5, Type.UInt32),
+ LedgerSequence(6, Type.UInt32),
+ CloseTime(7, Type.UInt32),
+ ParentCloseTime(8, Type.UInt32),
+ SigningTime(9, Type.UInt32),
+ Expiration(10, Type.UInt32),
+ TransferRate(11, Type.UInt32),
+ WalletSize(12, Type.UInt32),
+ OwnerCount(13, Type.UInt32),
+ DestinationTag(14, Type.UInt32),
+
+ HighQualityIn(16, Type.UInt32),
+ HighQualityOut(17, Type.UInt32),
+ LowQualityIn(18, Type.UInt32),
+ LowQualityOut(19, Type.UInt32),
+ QualityIn(20, Type.UInt32),
+ QualityOut(21, Type.UInt32),
+ StampEscrow(22, Type.UInt32),
+ BondAmount(23, Type.UInt32),
+ LoadFee(24, Type.UInt32),
+ OfferSequence(25, Type.UInt32),
+ FirstLedgerSequence(26, Type.UInt32), // Deprecated: do not use
+ // Added new semantics in 9486fc416ca7c59b8930b734266eed4d5b714c50
+ LastLedgerSequence(27, Type.UInt32),
+ TransactionIndex(28, Type.UInt32),
+ OperationLimit(29, Type.UInt32),
+ ReferenceFeeUnits(30, Type.UInt32),
+ ReserveBase(31, Type.UInt32),
+ ReserveIncrement(32, Type.UInt32),
+ SetFlag(33, Type.UInt32),
+ ClearFlag(34, Type.UInt32),
+ SignerQuorum(35, Type.UInt32),
+ CancelAfter(36, Type.UInt32),
+ FinishAfter(37, Type.UInt32),
+ SignerListID(38, Type.UInt32),
+ SettleDelay(39, Type.UInt32),
+
+ IndexNext(1, Type.UInt64),
+ IndexPrevious(2, Type.UInt64),
+ BookNode(3, Type.UInt64),
+ OwnerNode(4, Type.UInt64),
+ BaseFee(5, Type.UInt64),
+ ExchangeRate(6, Type.UInt64),
+ LowNode(7, Type.UInt64),
+ HighNode(8, Type.UInt64),
+ DestinationNode(9, Type.UInt64),
+ Cookie(10, Type.UInt64),
+
+ EmailHash(1, Type.Hash128),
+
+ LedgerHash(1, Type.Hash256),
+ ParentHash(2, Type.Hash256),
+ TransactionHash(3, Type.Hash256),
+ AccountHash(4, Type.Hash256),
+ PreviousTxnID(5, Type.Hash256),
+ LedgerIndex(6, Type.Hash256),
+ WalletLocator(7, Type.Hash256),
+ RootIndex(8, Type.Hash256),
+ // Added in rippled commit: 9486fc416ca7c59b8930b734266eed4d5b714c50
+ AccountTxnID(9, Type.Hash256),
+ BookDirectory(16, Type.Hash256),
+ InvoiceID(17, Type.Hash256),
+ Nickname(18, Type.Hash256),
+ Amendment(19, Type.Hash256),
+ TicketID(20, Type.Hash256),
+ Digest(21, Type.Hash256),
+ Channel(22, Type.Hash256),
+ ConsensusHash(23, Type.Hash256),
+ CheckID(24, Type.Hash256),
+
+ hash(257, Type.Hash256),
+ index(258, Type.Hash256),
+
+ Amount(1, Type.Amount),
+ Balance(2, Type.Amount),
+ LimitAmount(3, Type.Amount),
+ TakerPays(4, Type.Amount),
+ TakerGets(5, Type.Amount),
+ LowLimit(6, Type.Amount),
+ HighLimit(7, Type.Amount),
+ Fee(8, Type.Amount),
+ SendMax(9, Type.Amount),
+ DeliverMin(10, Type.Amount),
+
+ MinimumOffer(16, Type.Amount),
+ RippleEscrow(17, Type.Amount),
+ // Added in rippled commit: e7f0b8eca69dd47419eee7b82c8716b3aa5a9e39
+ DeliveredAmount(18, Type.Amount),
+ // These are auxiliary fields
+// quality(257, Type.AMOUNT),
+ taker_gets_funded(258, Type.Amount),
+ taker_pays_funded(259, Type.Amount),
+
+ PublicKey(1, Type.Blob),
+ MessageKey(2, Type.Blob),
+ SigningPubKey(3, Type.Blob),
+ TxnSignature(4, Type.Blob),
+ Signature(6, Type.Blob),
+ Domain(7, Type.Blob),
+ FundCode(8, Type.Blob),
+ RemoveCode(9, Type.Blob),
+ ExpireCode(10, Type.Blob),
+ CreateCode(11, Type.Blob),
+ MemoType(12, Type.Blob),
+ MemoData(13, Type.Blob),
+ MemoFormat(14, Type.Blob),
+ Fulfillment(16, Type.Blob),
+ Condition(17, Type.Blob),
+ MasterSignature(18, Type.Blob),
+
+ Account(1, Type.AccountID),
+ Owner(2, Type.AccountID),
+ Destination(3, Type.AccountID),
+ Issuer(4, Type.AccountID),
+ Authorize(5, Type.AccountID),
+ Unauthorize(6, Type.AccountID),
+ Target(7, Type.AccountID),
+ RegularKey(8, Type.AccountID),
+
+ ObjectEndMarker(1, Type.STObject),
+
+ TransactionMetaData(2, Type.STObject),
+ CreatedNode(3, Type.STObject),
+ DeletedNode(4, Type.STObject),
+ ModifiedNode(5, Type.STObject),
+ PreviousFields(6, Type.STObject),
+ FinalFields(7, Type.STObject),
+ NewFields(8, Type.STObject),
+ TemplateEntry(9, Type.STObject),
+ Memo(10, Type.STObject),
+ SignerEntry(11, Type.STObject),
+ Signer(16, Type.STObject),
+ // 17 unused
+ Majority(18, Type.STObject),
+
+ ArrayEndMarker(1, Type.STArray),
+// SigningAccounts(2, Type.STArray),
+ Signers(3, Type.STArray),
+ SignerEntries(4, Type.STArray),
+ Template(5, Type.STArray),
+ Necessary(6, Type.STArray),
+ Sufficient(7, Type.STArray),
+ AffectedNodes(8, Type.STArray),
+ Memos(9, Type.STArray),
+ Majorities(16, Type.STArray),
+
+ CloseResolution(1, Type.UInt8),
+ Method(2, Type.UInt8),
+ TransactionResult(3, Type.UInt8),
+ TickSize(16, Type.UInt8),
+
+ TakerPaysCurrency(1, Type.Hash160),
+ TakerPaysIssuer(2, Type.Hash160),
+ TakerGetsCurrency(3, Type.Hash160),
+ TakerGetsIssuer(4, Type.Hash160),
+
+ Paths(1, Type.PathSet),
+
+ Indexes(1, Type.Vector256),
+ Hashes(2, Type.Vector256),
+ Amendments(3, Type.Vector256),
+
+ Transaction(1, Type.Transaction),
+ LedgerEntry(1, Type.LedgerEntry),
+ Validation(1, Type.Validation);
+
+ final int id;
+
+ // defaults
+ boolean signingField = true;
+ boolean isSerialized = true;
+ boolean isVlEncoded = false;
+
+ public static Field fromString(String key) {
+ Field f;
+ try {
+ f = valueOf(key);
+ } catch (IllegalArgumentException e) {
+ f = null;
+ }
+ return f;
+ }
+
+ private static byte[] asBytes(Field field) {
+ int name = field.getId(), type = field.getType().getId();
+ ArrayList header = new ArrayList<>(3);
+
+ if (type < 16)
+ {
+ if (name < 16) // common type, common name
+ header.add((byte)((type << 4) | name));
+ else
+ {
+ // common type, uncommon name
+ header.add((byte)(type << 4));
+ header.add((byte)(name));
+ }
+ }
+ else if (name < 16)
+ {
+ // uncommon type, common name
+ header.add((byte)(name));
+ header.add((byte)(type));
+ }
+ else
+ {
+ // uncommon type, uncommon name
+ header.add((byte)(0));
+ header.add((byte)(type));
+ header.add((byte)(name));
+ }
+
+ byte[] headerBytes = new byte[header.size()];
+ for (int i = 0; i < header.size(); i++) {
+ headerBytes[i] = header.get(i);
+ }
+
+ return headerBytes;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ final int code;
+ final Type type;
+ private final byte[] bytes;
+ public Object tag = null;
+
+ Field(int fid, Type tid) {
+ id = fid;
+ type = tid;
+ code = (type.id << 16) | fid;
+ isSerialized = isSerialized(this);
+
+ if (isSerialized()) {
+ bytes = asBytes(this);
+ } else {
+ bytes = null;
+ }
+ }
+
+ static private Map byCode = new TreeMap<>();
+
+ static public Field fromCode(Integer integer) {
+ return byCode.get(integer);
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public boolean isSerialized() {
+ return isSerialized;
+ }
+ public boolean isVLEncoded() {
+ return isVlEncoded;
+ }
+ public boolean isSigningField() {
+ return signingField;
+ }
+ private static boolean isSerialized(Field f) {
+ // This should screen out `hash` and `index` and the like
+ return ((f.type.id > 0) && (f.type.id < 256) && (f.id > 0) && (f.id < 256));
+ }
+
+ static public Comparator comparator = Comparator.comparingInt(o -> o.code);
+
+ static {
+ for (Field f : Field.values()) {
+ byCode.put(f.code, f);
+ f.isSerialized = isSerialized(f);
+ f.signingField = f.isSerialized;
+
+ switch (f.type) {
+ case Blob:
+ case AccountID:
+ case Vector256:
+ f.isVlEncoded = true;
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ TxnSignature.signingField = false;
+ Signers.signingField = false;
+ Signature.signingField = false;
+ MasterSignature.signingField = false;
+
+ ArrayList sortedFields;
+ Field[] values = Field.values();
+ sortedFields = new ArrayList<>(Arrays.asList(values));
+ sortedFields.sort(comparator);
+
+ for (int i = 0; i < values.length; i++) {
+ Field av = values[i];
+ Field lv = sortedFields.get(i);
+ if (av.code != lv.code) {
+ throw new AssertionError(
+ "Field enum declaration isn't presorted");
+ }
+ }
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+}
\ No newline at end of file
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/HasField.java b/ripple-core/src/main/java/com/ripple/core/fields/HasField.java
new file mode 100644
index 0000000000..edc49029a3
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/HasField.java
@@ -0,0 +1,5 @@
+package com.ripple.core.fields;
+
+public interface HasField {
+ Field getField();
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Hash128Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Hash128Field.java
new file mode 100644
index 0000000000..b8b78e912e
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/Hash128Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class Hash128Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Hash160Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Hash160Field.java
new file mode 100644
index 0000000000..f0569b13eb
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/Hash160Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class Hash160Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Hash256Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Hash256Field.java
new file mode 100644
index 0000000000..e0a83b67ef
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/Hash256Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class Hash256Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/PathSetField.java b/ripple-core/src/main/java/com/ripple/core/fields/PathSetField.java
new file mode 100644
index 0000000000..5982570eb6
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/PathSetField.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class PathSetField implements HasField{}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/STArrayField.java b/ripple-core/src/main/java/com/ripple/core/fields/STArrayField.java
new file mode 100644
index 0000000000..056a55c861
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/STArrayField.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class STArrayField implements HasField{}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/STObjectField.java b/ripple-core/src/main/java/com/ripple/core/fields/STObjectField.java
new file mode 100644
index 0000000000..c802f57a2a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/STObjectField.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class STObjectField implements HasField{}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Type.java b/ripple-core/src/main/java/com/ripple/core/fields/Type.java
new file mode 100644
index 0000000000..2384861e88
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/Type.java
@@ -0,0 +1,35 @@
+/* DO NOT EDIT, AUTO GENERATED */
+package com.ripple.core.fields;
+
+public enum Type {
+ Unknown(-2),
+ Done(-1),
+ NotPresent(0),
+ UInt16(1),
+ UInt32(2),
+ UInt64(3),
+ Hash128(4),
+ Hash256(5),
+ Amount(6),
+ Blob(7),
+ AccountID(8),
+ STObject(14),
+ STArray(15),
+ UInt8(16),
+ Hash160(17),
+ PathSet(18),
+ Vector256(19),
+ Transaction(10001),
+ LedgerEntry(10002),
+ Validation(10003);
+
+ final int id;
+
+ Type(int type) {
+ this.id = type;
+ }
+
+ public int getId() {
+ return id;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt16Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt16Field.java
new file mode 100644
index 0000000000..fa99de6499
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt16Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class UInt16Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt32Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt32Field.java
new file mode 100644
index 0000000000..a2b58902c2
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt32Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class UInt32Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt64Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt64Field.java
new file mode 100644
index 0000000000..3c85f3b312
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt64Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class UInt64Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/UInt8Field.java b/ripple-core/src/main/java/com/ripple/core/fields/UInt8Field.java
new file mode 100644
index 0000000000..465a1ad6be
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/UInt8Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class UInt8Field implements HasField {}
diff --git a/ripple-core/src/main/java/com/ripple/core/fields/Vector256Field.java b/ripple-core/src/main/java/com/ripple/core/fields/Vector256Field.java
new file mode 100644
index 0000000000..7cadc88ea8
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/fields/Vector256Field.java
@@ -0,0 +1,3 @@
+package com.ripple.core.fields;
+
+public abstract class Vector256Field implements HasField{}
diff --git a/ripple-core/src/main/java/com/ripple/core/formats/Format.java b/ripple-core/src/main/java/com/ripple/core/formats/Format.java
new file mode 100644
index 0000000000..0061213330
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/formats/Format.java
@@ -0,0 +1,66 @@
+package com.ripple.core.formats;
+
+import com.ripple.core.fields.Field;
+
+import java.util.EnumMap;
+
+abstract public class Format {
+ protected Format() {
+ }
+
+ protected void addCommonFields(){}
+
+ EnumMap requirementEnumMap = new EnumMap<>(Field.class);
+ EnumMap common = new EnumMap<>(Field.class);
+
+ public EnumMap requirements() {
+ return requirementEnumMap;
+ }
+
+ abstract public String name ();
+
+ public Format(Object[] args) {
+ if ((!(args.length % 2 == 0)) || args.length < 2) {
+ throw new IllegalArgumentException("Varargs length should be a minimum multiple of 2");
+ }
+ for (int i = 0; i < args.length; i+= 2) {
+ Field f = (Field) args[i];
+ Requirement r = (Requirement) args[i + 1];
+ put(f, r);
+ }
+ }
+
+ protected void put(Field f, Requirement r) {
+ requirementEnumMap.put(f, r);
+ }
+
+ @SuppressWarnings("unchecked")
+ Subclass required(Field f) {
+ put(f, Requirement.REQUIRED);
+ return (Subclass) this;
+ }
+
+ @SuppressWarnings("unchecked")
+ Subclass optional(Field f) {
+ put(f, Requirement.OPTIONAL);
+ return (Subclass) this;
+ }
+
+ @SuppressWarnings("unchecked")
+ Subclass nonDefault(Field f) {
+ put(f, Requirement.DEFAULT);
+ return (Subclass) this;
+ }
+
+ public boolean isCommon(Field field) {
+ return common.containsKey(field);
+ }
+
+ public static enum Requirement {
+ INVALID(-1),
+ REQUIRED( 0),
+ OPTIONAL( 1),
+ DEFAULT( 2);
+ Requirement(int i) {}
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/formats/LEFormat.java b/ripple-core/src/main/java/com/ripple/core/formats/LEFormat.java
new file mode 100644
index 0000000000..2dbd576593
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/formats/LEFormat.java
@@ -0,0 +1,230 @@
+package com.ripple.core.formats;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+import java.util.EnumMap;
+
+public class LEFormat extends Format {
+ static public EnumMap formats = new EnumMap(LedgerEntryType.class);
+ {
+ common.put(Field.LedgerIndex, Requirement.OPTIONAL);
+ common.put(Field.LedgerEntryType, Requirement.REQUIRED);
+ common.put(Field.Flags, Requirement.REQUIRED);
+ }
+
+ static public LEFormat fromString(String name) {
+ return getLedgerFormat(LedgerEntryType.valueOf(name));
+ }
+
+ static public LEFormat fromNumber(Number ord) {
+ return getLedgerFormat(LedgerEntryType.fromNumber(ord));
+ }
+
+ static public LEFormat fromValue(Object o) {
+ if (o instanceof Number) {
+ return fromNumber(((Number) o).intValue());
+ } else if (o instanceof String){
+ return fromString((String) o);
+ }
+ else {
+ return null;
+ }
+ }
+
+ public static LEFormat getLedgerFormat(LedgerEntryType key) {
+ if (key == null) return null;
+ return formats.get(key);
+ }
+
+ public final LedgerEntryType ledgerEntryType;
+
+ public LEFormat(LedgerEntryType type, Object... args) {
+ super(args);
+ ledgerEntryType = type;
+ addCommonFields();
+ formats.put(type, this);
+ }
+
+ public LEFormat(LedgerEntryType type) {
+ super();
+ ledgerEntryType = type;
+ addCommonFields();
+ formats.put(type, this);
+ }
+
+ @Override
+ protected void addCommonFields() {
+ requirementEnumMap.putAll(common);
+ }
+
+ @Override
+ public String name() {
+ return ledgerEntryType.toString();
+ }
+
+ public static LEFormat AccountRoot = new LEFormat(
+ LedgerEntryType.AccountRoot,
+ Field.Account, Requirement.REQUIRED,
+ Field.Sequence, Requirement.REQUIRED,
+ Field.Balance, Requirement.REQUIRED,
+ Field.OwnerCount, Requirement.REQUIRED,
+ Field.PreviousTxnID, Requirement.REQUIRED,
+ Field.PreviousTxnLgrSeq, Requirement.REQUIRED,
+ Field.AccountTxnID, Requirement.OPTIONAL,
+ Field.RegularKey, Requirement.OPTIONAL,
+ Field.EmailHash, Requirement.OPTIONAL,
+ Field.WalletLocator, Requirement.OPTIONAL,
+ Field.WalletSize, Requirement.OPTIONAL,
+ Field.MessageKey, Requirement.OPTIONAL,
+ Field.TransferRate, Requirement.OPTIONAL,
+ Field.Domain, Requirement.OPTIONAL,
+ Field.TickSize, Requirement.OPTIONAL
+ );
+
+ public static LEFormat DirectoryNode = new LEFormat(
+ LedgerEntryType.DirectoryNode,
+ Field.Owner, Requirement.OPTIONAL, // for owner directories
+ Field.TakerPaysCurrency, Requirement.OPTIONAL, // for order book directories
+ Field.TakerPaysIssuer, Requirement.OPTIONAL, // for order book directories
+ Field.TakerGetsCurrency, Requirement.OPTIONAL, // for order book directories
+ Field.TakerGetsIssuer, Requirement.OPTIONAL, // for order book directories
+ Field.ExchangeRate, Requirement.OPTIONAL, // for order book directories
+ Field.Indexes, Requirement.REQUIRED,
+ Field.RootIndex, Requirement.REQUIRED,
+ Field.IndexNext, Requirement.OPTIONAL,
+ Field.IndexPrevious, Requirement.OPTIONAL
+ );
+
+
+ public static LEFormat Offer = new LEFormat(
+ LedgerEntryType.Offer,
+ Field.Account, Requirement.REQUIRED,
+ Field.Sequence, Requirement.REQUIRED,
+ Field.TakerPays, Requirement.REQUIRED,
+ Field.TakerGets, Requirement.REQUIRED,
+ Field.BookDirectory, Requirement.REQUIRED,
+ Field.BookNode, Requirement.REQUIRED,
+ Field.OwnerNode, Requirement.REQUIRED,
+ Field.PreviousTxnID, Requirement.REQUIRED,
+ Field.PreviousTxnLgrSeq, Requirement.REQUIRED,
+ Field.Expiration, Requirement.OPTIONAL
+ );
+
+ public static LEFormat Ticket = new LEFormat(
+ LedgerEntryType.Ticket,
+ Field.Account, Requirement.REQUIRED,
+ Field.Sequence, Requirement.REQUIRED,
+ Field.OwnerNode, Requirement.REQUIRED,
+ Field.Target, Requirement.OPTIONAL,
+ Field.Expiration, Requirement.OPTIONAL
+ );
+
+ public static LEFormat RippleState = new LEFormat(
+ LedgerEntryType.RippleState,
+ Field.Balance, Requirement.REQUIRED,
+ Field.LowLimit, Requirement.REQUIRED,
+ Field.HighLimit, Requirement.REQUIRED,
+ Field.PreviousTxnID, Requirement.REQUIRED,
+ Field.PreviousTxnLgrSeq, Requirement.REQUIRED,
+ Field.LowNode, Requirement.OPTIONAL,
+ Field.LowQualityIn, Requirement.OPTIONAL,
+ Field.LowQualityOut, Requirement.OPTIONAL,
+ Field.HighNode, Requirement.OPTIONAL,
+ Field.HighQualityIn, Requirement.OPTIONAL,
+ Field.HighQualityOut, Requirement.OPTIONAL
+ );
+
+ public static LEFormat Escrow = new LEFormat(
+ LedgerEntryType.Escrow,
+ Field.Account, Requirement.REQUIRED,
+ Field.Destination, Requirement.REQUIRED,
+ Field.Amount, Requirement.REQUIRED,
+
+ Field.PreviousTxnID, Requirement.REQUIRED,
+ Field.PreviousTxnLgrSeq, Requirement.REQUIRED,
+ Field.OwnerNode, Requirement.REQUIRED,
+
+ Field.Condition, Requirement.OPTIONAL,
+ Field.CancelAfter, Requirement.OPTIONAL,
+ Field.FinishAfter, Requirement.OPTIONAL,
+ Field.SourceTag, Requirement.OPTIONAL,
+ Field.DestinationTag, Requirement.OPTIONAL,
+ Field.DestinationNode, Requirement.OPTIONAL
+ );
+
+ public static LEFormat LedgerHashes = new LEFormat(
+ LedgerEntryType.LedgerHashes,
+ Field.FirstLedgerSequence, Requirement.OPTIONAL, // Remove if we do a ledger restart
+ Field.LastLedgerSequence, Requirement.OPTIONAL,
+ Field.Hashes, Requirement.REQUIRED
+ );
+
+ public static LEFormat Amendments = new LEFormat(
+ LedgerEntryType.Amendments,
+ Field.Amendments, Requirement.OPTIONAL,
+ Field.Majorities, Requirement.OPTIONAL
+ );
+
+ public static LEFormat SignerList = new LEFormat(
+ LedgerEntryType.SignerList,
+
+ Field.PreviousTxnID, Requirement.REQUIRED,
+ Field.PreviousTxnLgrSeq, Requirement.REQUIRED,
+ Field.OwnerNode, Requirement.REQUIRED,
+
+ Field.SignerQuorum, Requirement.REQUIRED,
+ Field.SignerEntries, Requirement.REQUIRED,
+ Field.SignerListID, Requirement.REQUIRED
+ );
+
+ public static LEFormat FeeSettings = new LEFormat(
+ LedgerEntryType.FeeSettings,
+ Field.BaseFee, Requirement.REQUIRED,
+ Field.ReferenceFeeUnits, Requirement.REQUIRED,
+ Field.ReserveBase, Requirement.REQUIRED,
+ Field.ReserveIncrement, Requirement.REQUIRED
+ );
+
+ public static LEFormat PayChannel = new LEFormat(
+ LedgerEntryType.PayChannel,
+ Field.Account, Requirement.REQUIRED,
+ Field.Destination, Requirement.REQUIRED,
+ Field.Amount, Requirement.REQUIRED,
+ Field.Balance, Requirement.REQUIRED,
+ Field.PublicKey, Requirement.REQUIRED,
+ Field.SettleDelay, Requirement.REQUIRED,
+ Field.Expiration, Requirement.OPTIONAL,
+ Field.CancelAfter, Requirement.OPTIONAL,
+ Field.SourceTag, Requirement.OPTIONAL,
+ Field.DestinationTag, Requirement.OPTIONAL,
+ Field.OwnerNode, Requirement.REQUIRED,
+ Field.PreviousTxnID, Requirement.REQUIRED,
+ Field.PreviousTxnLgrSeq, Requirement.REQUIRED
+ );
+
+ public static LEFormat Check = new LEFormat(LedgerEntryType.Check)
+ .required(Field.PreviousTxnID)
+ .required(Field.PreviousTxnLgrSeq)
+ .required(Field.Account)
+ .required(Field.Destination)
+ .required(Field.SendMax)
+ .required(Field.Sequence)
+ .required(Field.OwnerNode)
+ .required(Field.DestinationNode)
+ .optional(Field.Expiration)
+ .optional(Field.InvoiceID)
+ .optional(Field.SourceTag)
+ .optional(Field.DestinationTag)
+
+ ;
+
+ public static LEFormat DepositPreauth = new LEFormat(LedgerEntryType.DepositPreauth)
+ .required(Field.Account)
+ .required(Field.Authorize)
+ .required(Field.OwnerNode)
+ .required(Field.PreviousTxnID)
+ .required(Field.PreviousTxnLgrSeq)
+ ;
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/formats/TxFormat.java b/ripple-core/src/main/java/com/ripple/core/formats/TxFormat.java
new file mode 100644
index 0000000000..fa66d27533
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/formats/TxFormat.java
@@ -0,0 +1,212 @@
+package com.ripple.core.formats;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.TransactionType;
+
+import java.util.EnumMap;
+
+public class TxFormat extends Format {
+ static public EnumMap formats = new EnumMap<>(TransactionType.class);
+ {
+ common.put(Field.TransactionType, Requirement.REQUIRED);
+ common.put(Field.Account, Requirement.REQUIRED);
+ common.put(Field.Sequence, Requirement.REQUIRED);
+ common.put(Field.Fee, Requirement.REQUIRED);
+ common.put(Field.SigningPubKey, Requirement.REQUIRED);
+
+ common.put(Field.Flags, Requirement.OPTIONAL);
+ common.put(Field.SourceTag, Requirement.OPTIONAL);
+ common.put(Field.PreviousTxnID, Requirement.OPTIONAL);
+ common.put(Field.OperationLimit, Requirement.OPTIONAL);
+ common.put(Field.TxnSignature, Requirement.OPTIONAL);
+ common.put(Field.AccountTxnID, Requirement.OPTIONAL);
+ common.put(Field.LastLedgerSequence, Requirement.OPTIONAL);
+ common.put(Field.Memos, Requirement.OPTIONAL);
+ common.put(Field.Signers, Requirement.OPTIONAL);
+
+ }
+ public final TransactionType transactionType;
+
+ static public TxFormat fromString(String name) {
+ return getTxFormat(TransactionType.valueOf(name));
+ }
+
+ static public TxFormat fromNumber(Number ord) {
+ return getTxFormat(TransactionType.fromNumber(ord));
+ }
+
+ private static TxFormat getTxFormat(TransactionType key) {
+ if (key == null) return null;
+ return formats.get(key);
+ }
+
+ public TxFormat(TransactionType type, Object... args) {
+ super(args);
+ transactionType = type;
+ addCommonFields();
+ formats.put(transactionType, this);
+ }
+
+ public TxFormat(TransactionType type) {
+ super();
+ transactionType = type;
+ addCommonFields();
+ formats.put(transactionType, this);
+ }
+
+ @Override
+ protected void addCommonFields() {
+ requirementEnumMap.putAll(common);
+ }
+
+ @Override
+ public String name() {
+ return transactionType.toString();
+ }
+
+ private static TxFormat makeFormat(TransactionType tt) {
+ return new TxFormat(tt);
+ }
+
+ static public TxFormat AccountSet = makeFormat(
+ TransactionType.AccountSet)
+ .optional(Field.EmailHash)
+ .optional(Field.EmailHash)
+ .optional(Field.WalletLocator)
+ .optional(Field.WalletSize)
+ .optional(Field.MessageKey)
+ .optional(Field.Domain)
+ .optional(Field.TransferRate)
+ .optional(Field.SetFlag)
+ .optional(Field.TickSize)
+ .optional(Field.ClearFlag);
+
+ static public TxFormat TrustSet = new TxFormat(
+ TransactionType.TrustSet,
+ Field.LimitAmount, Requirement.OPTIONAL,
+ Field.QualityIn, Requirement.OPTIONAL,
+ Field.QualityOut, Requirement.OPTIONAL);
+
+ static public TxFormat OfferCreate = new TxFormat(
+ TransactionType.OfferCreate,
+ Field.TakerPays, Requirement.REQUIRED,
+ Field.TakerGets, Requirement.REQUIRED,
+ Field.Expiration, Requirement.OPTIONAL,
+ Field.OfferSequence, Requirement.OPTIONAL);
+
+ static public TxFormat OfferCancel = new TxFormat(
+ TransactionType.OfferCancel,
+ Field.OfferSequence, Requirement.REQUIRED);
+
+ static public TxFormat TicketCreate = new TxFormat(
+ TransactionType.TicketCreate,
+ Field.Target, Requirement.OPTIONAL,
+ Field.Expiration, Requirement.OPTIONAL);
+
+ static public TxFormat TicketCancel = new TxFormat(
+ TransactionType.TicketCancel,
+ Field.TicketID, Requirement.REQUIRED);
+
+ static public TxFormat SetRegularKey = new TxFormat(
+ TransactionType.SetRegularKey,
+ Field.RegularKey, Requirement.OPTIONAL);
+
+ static public TxFormat Payment = new TxFormat(
+ TransactionType.Payment,
+ Field.Destination, Requirement.REQUIRED,
+ Field.Amount, Requirement.REQUIRED,
+ Field.SendMax, Requirement.OPTIONAL,
+ Field.Paths, Requirement.DEFAULT,
+ Field.InvoiceID, Requirement.OPTIONAL,
+ Field.DestinationTag, Requirement.OPTIONAL,
+ Field.DeliverMin, Requirement.OPTIONAL
+ );
+
+
+ static public TxFormat EscrowCreate = new TxFormat(
+ TransactionType.EscrowCreate,
+ Field.Destination, Requirement.REQUIRED,
+ Field.Amount, Requirement.REQUIRED,
+ Field.Condition, Requirement.OPTIONAL,
+ Field.CancelAfter, Requirement.OPTIONAL,
+ Field.FinishAfter, Requirement.OPTIONAL,
+ Field.DestinationTag, Requirement.OPTIONAL);
+
+ static public TxFormat EscrowFinish = new TxFormat(
+ TransactionType.EscrowFinish,
+ Field.Owner, Requirement.REQUIRED,
+ Field.OfferSequence, Requirement.REQUIRED,
+ Field.Fulfillment, Requirement.OPTIONAL,
+ Field.Condition, Requirement.OPTIONAL);
+
+ static public TxFormat EscrowCancel = new TxFormat(
+ TransactionType.EscrowCancel,
+ Field.Owner, Requirement.REQUIRED,
+ Field.OfferSequence, Requirement.REQUIRED);
+
+ static public TxFormat EnableAmendment = new TxFormat(
+ TransactionType.EnableAmendment,
+ Field.LedgerSequence, Requirement.REQUIRED,
+ Field.Amendment, Requirement.REQUIRED);
+
+ static public TxFormat SetFee = new TxFormat(
+ TransactionType.SetFee,
+ Field.BaseFee, Requirement.REQUIRED,
+ Field.ReferenceFeeUnits, Requirement.REQUIRED,
+ Field.ReserveBase, Requirement.REQUIRED,
+ Field.LedgerSequence, Requirement.OPTIONAL,
+ Field.ReserveIncrement, Requirement.REQUIRED
+ );
+
+ static public TxFormat SignerListSet = new TxFormat(
+ TransactionType.SignerListSet,
+ Field.SignerQuorum, Requirement.REQUIRED,
+ Field.SignerEntries, Requirement.OPTIONAL
+ );
+ static public TxFormat PaymentChannelCreate = new TxFormat(
+ TransactionType.PaymentChannelCreate,
+ Field.Destination, Requirement.REQUIRED,
+ Field.Amount, Requirement.REQUIRED,
+ Field.SettleDelay, Requirement.REQUIRED,
+ Field.PublicKey, Requirement.REQUIRED,
+ Field.CancelAfter, Requirement.OPTIONAL,
+ Field.DestinationTag, Requirement.OPTIONAL
+ );
+ static public TxFormat PaymentChannelFund = new TxFormat(
+ TransactionType.PaymentChannelFund,
+ Field.Channel, Requirement.REQUIRED,
+ Field.Amount, Requirement.REQUIRED,
+ Field.Expiration, Requirement.OPTIONAL
+ );
+ static public TxFormat PaymentChannelClaim = new TxFormat(
+ TransactionType.PaymentChannelClaim,
+ Field.Channel, Requirement.REQUIRED,
+ Field.Amount, Requirement.OPTIONAL,
+ Field.Balance, Requirement.OPTIONAL,
+ Field.Signature, Requirement.OPTIONAL,
+ Field.PublicKey, Requirement.OPTIONAL
+ );
+
+ static public TxFormat CheckCreate = new TxFormat(TransactionType.CheckCreate)
+ .required(Field.Destination)
+ .required(Field.SendMax)
+ .optional(Field.Expiration)
+ .optional(Field.DestinationTag)
+ .optional(Field.InvoiceID)
+ ;
+
+ static public TxFormat CheckCash = new TxFormat(TransactionType.CheckCash)
+ .required(Field.CheckID)
+ .optional(Field.Amount)
+ .optional(Field.DeliverMin)
+ ;
+
+ static public TxFormat CheckCancel = new TxFormat(TransactionType.CheckCancel)
+ .required(Field.CheckID)
+ ;
+
+ static public TxFormat DepositPreauth = new TxFormat(TransactionType.DepositPreauth)
+ .optional(Field.Authorize)
+ .optional(Field.Unauthorize)
+ ;
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/runtime/Value.java b/ripple-core/src/main/java/com/ripple/core/runtime/Value.java
new file mode 100644
index 0000000000..1f69e88c55
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/runtime/Value.java
@@ -0,0 +1,111 @@
+package com.ripple.core.runtime;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+
+public enum Value {
+ UNKNOWN,
+ STRING,
+ JSON_OBJECT,
+ JSON_ARRAY,
+
+ JACKSON_ARRAY,
+ JACKSON_BINARY,
+ JACKSON_BOOLEAN,
+ JACKSON_MISSING,
+ JACKSON_NULL,
+ JACKSON_NUMBER,
+ JACKSON_OBJECT,
+ JACKSON_POJO,
+ JACKSON_STRING,
+
+ LIST,
+ MAP,
+ NUMBER,
+ BYTE,
+ DOUBLE,
+ FLOAT,
+ INTEGER,
+ LONG,
+ BYTE_ARRAY,
+ SHORT,
+ BOOLEAN;
+
+ static public Value typeOf (Object object) {
+ if (object instanceof String) {
+ return STRING;
+ }
+ else if (object instanceof Number) {
+ if (object instanceof Byte) {
+ return BYTE;
+ }
+ else if (object instanceof Double) {
+ return DOUBLE;
+ }
+ else if (object instanceof Float) {
+ return FLOAT;
+ }
+ else if (object instanceof Integer) {
+ return INTEGER;
+ }
+ else if (object instanceof Long) {
+ return LONG;
+ }
+ else if (object instanceof Short) {
+ return SHORT;
+ }
+ return NUMBER;
+ }
+ else if (object instanceof JSONObject) {
+ return JSON_OBJECT;
+ }
+ else if (object instanceof JSONArray) {
+ return JSON_ARRAY;
+ }
+ else if (object instanceof JsonNode) {
+ JsonNode node = (JsonNode) object;
+ JsonNodeType nodeType = node.getNodeType();
+ switch (nodeType) {
+ case ARRAY:
+ return JACKSON_ARRAY;
+ case BINARY:
+ return JACKSON_BINARY;
+ case BOOLEAN:
+ return JACKSON_BOOLEAN;
+ case MISSING:
+ return JACKSON_MISSING;
+ case NULL:
+ return JACKSON_NULL;
+ case NUMBER:
+ return JACKSON_NUMBER;
+ case OBJECT:
+ return JACKSON_OBJECT;
+ case POJO:
+ return JACKSON_POJO;
+ case STRING:
+ return JACKSON_STRING;
+ }
+ throw new IllegalStateException("unknown node type");
+ }
+ else if (object instanceof Map) {
+ return MAP;
+ }
+ else if (object instanceof Boolean) {
+ return BOOLEAN;
+ }
+ else if (object instanceof List) {
+ return LIST;
+ }
+ else if (object instanceof byte[]) {
+ return BYTE_ARRAY;
+ }
+ else {
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BinaryParser.java b/ripple-core/src/main/java/com/ripple/core/serialized/BinaryParser.java
new file mode 100644
index 0000000000..86fa08a7d9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/BinaryParser.java
@@ -0,0 +1,99 @@
+package com.ripple.core.serialized;
+
+import com.ripple.core.fields.Field;
+import com.ripple.encodings.common.B16;
+
+public class BinaryParser {
+ protected final int size;
+ protected byte[] bytes;
+ protected int cursor = 0;
+
+ public BinaryParser(byte[] bytes) {
+ this.size = bytes.length;
+ this.bytes = bytes;
+ }
+
+ public BinaryParser(int size) {
+ this.size = size;
+ }
+
+ public BinaryParser(String hex) {
+ this(B16.decode(hex));
+ }
+
+ public void skip(int n) {
+ cursor += n;
+ }
+ public byte readOne() {
+ return bytes[cursor++];
+ }
+ public byte[] read(int n) {
+ byte[] ret = new byte[n];
+ System.arraycopy(bytes, cursor, ret, 0, n);
+ cursor += n;
+// }
+ return ret;
+ }
+
+ public Field readField() {
+ int fieldCode = readFieldCode();
+ Field field = Field.fromCode(fieldCode);
+ if (field == null) {
+ throw new IllegalStateException("Couldn't parse field from " +
+ Integer.toHexString(fieldCode));
+ }
+ return field;
+ }
+
+ public int readFieldCode() {
+ byte tagByte = readOne();
+
+ int typeBits = (tagByte & 0xFF) >>> 4;
+ if (typeBits == 0) typeBits = readOne();
+
+ int fieldBits = tagByte & 0x0F;
+ if (fieldBits == 0) fieldBits = readOne();
+
+ return (typeBits << 16 | fieldBits);
+ }
+
+ public boolean end() {
+ return cursor >= size; // greater guard against infinite loops
+ }
+
+ public int pos() {
+ return cursor;
+ }
+
+ public int readOneInt() {
+ return readOne() & 0xFF;
+ }
+
+ public int readVLLength() {
+ int b1 = readOneInt();
+ int result;
+
+ if (b1 <= 192) {
+ result = b1;
+ } else if (b1 <= 240) {
+ int b2 = readOneInt();
+ result = 193 + (b1 - 193) * 256 + b2;
+ } else if (b1 <= 254) {
+ int b2 = readOneInt();
+ int b3 = readOneInt();
+ result = 12481 + (b1 - 241) * 65536 + b2 * 256 + b3;
+ } else {
+ throw new RuntimeException("Invalid varint length indicator");
+ }
+
+ return result;
+ }
+
+ public int size() {
+ return size;
+ }
+
+ public boolean end(Integer customEnd) {
+ return cursor >= size || customEnd != null && cursor >= customEnd;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BinarySerializer.java b/ripple-core/src/main/java/com/ripple/core/serialized/BinarySerializer.java
new file mode 100644
index 0000000000..1b39a2ac10
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/BinarySerializer.java
@@ -0,0 +1,94 @@
+package com.ripple.core.serialized;
+
+import com.ripple.core.fields.Field;
+import com.ripple.core.fields.Type;
+
+import java.util.Arrays;
+
+public class BinarySerializer {
+ private final BytesSink sink;
+
+ public BinarySerializer(BytesSink sink) {
+ this.sink = sink;
+ }
+
+ public void add(byte[] n) {
+ sink.add(n);
+ }
+
+ public void addLengthEncoded(byte[] n) {
+ add(encodeVL(n.length));
+ add(n);
+ }
+
+ public static byte[] encodeVL(int length) {
+ // TODO: bytes
+ byte[] lenBytes = new byte[4];
+
+ if (length <= 192)
+ {
+ lenBytes[0] = (byte) (length);
+ return Arrays.copyOf(lenBytes, 1);
+ }
+ else if (length <= 12480)
+ {
+ length -= 193;
+ lenBytes[0] = (byte) (193 + (length >>> 8));
+ lenBytes[1] = (byte) (length & 0xff);
+ return Arrays.copyOf(lenBytes, 2);
+ }
+ else if (length <= 918744) {
+ length -= 12481;
+ lenBytes[0] = (byte) (241 + (length >>> 16));
+ lenBytes[1] = (byte) ((length >> 8) & 0xff);
+ lenBytes[2] = (byte) (length & 0xff);
+ return Arrays.copyOf(lenBytes, 3);
+ } else {
+ throw new RuntimeException("Overflow error");
+ }
+ }
+
+ public void add(BytesList bl) {
+ for (byte[] bytes : bl.rawList()) {
+ sink.add(bytes);
+ }
+ }
+
+ public int addFieldHeader(Field f) {
+ if (!f.isSerialized()) {
+ throw new IllegalStateException(String.format("Field %s is a discardable field", f));
+ }
+ byte[] n = f.getBytes();
+ add(n);
+ return n.length;
+ }
+
+ public void add(byte type) {
+ sink.add(type);
+ }
+
+ public void addLengthEncoded(BytesList bytes) {
+ add(encodeVL(bytes.bytesLength()));
+ add(bytes);
+ }
+
+ public void add(Field field, SerializedType value) {
+ addFieldHeader(field);
+ if (field.isVLEncoded()) {
+ addLengthEncoded(value);
+ } else {
+ value.toBytesSink(sink);
+ if (field.getType() == Type.STObject) {
+ addFieldHeader(Field.ObjectEndMarker);
+ } else if (field.getType() == Type.STArray) {
+ addFieldHeader(Field.ArrayEndMarker);
+ }
+ }
+ }
+
+ public void addLengthEncoded(SerializedType value) {
+ BytesList bytes = new BytesList();
+ value.toBytesSink(bytes);
+ addLengthEncoded(bytes);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BytesList.java b/ripple-core/src/main/java/com/ripple/core/serialized/BytesList.java
new file mode 100644
index 0000000000..69d150aa16
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/BytesList.java
@@ -0,0 +1,77 @@
+package com.ripple.core.serialized;
+
+import java.security.MessageDigest;
+import java.util.ArrayList;
+
+public class BytesList implements BytesSink {
+ private ArrayList buffer = new ArrayList();
+
+ private int len = 0;
+
+ public void add(BytesList bl) {
+ for (byte[] bytes : bl.rawList()) {
+ add(bytes);
+ }
+ }
+
+ @Override
+ public void add(byte aByte) {
+ add(new byte[]{aByte});
+ }
+
+ @Override
+ public void add(byte[] bytes) {
+ len += bytes.length;
+ buffer.add(bytes);
+ }
+
+ public byte[] bytes() {
+ int n = bytesLength();
+ byte[] bytes = new byte[n];
+ addBytes(bytes, 0);
+ return bytes;
+ }
+
+ static public String[] hexLookup = new String[256];
+ static {
+ for (int i = 0; i < 256; i++) {
+ String s = Integer.toHexString(i).toUpperCase();
+ if (s.length() == 1) {
+ s = "0" + s;
+ }
+ hexLookup[i] = s;
+ }
+ }
+
+ public String bytesHex() {
+ StringBuilder builder = new StringBuilder(len * 2);
+ for (byte[] buf : buffer) {
+ for (byte aBytes : buf) {
+ builder.append(hexLookup[aBytes & 0xFF]);
+ }
+ }
+ return builder.toString();
+ }
+
+ public int bytesLength() {
+ return len;
+ }
+
+ private int addBytes(byte[] bytes, int destPos) {
+ for (byte[] buf : buffer) {
+ System.arraycopy(buf, 0, bytes, destPos, buf.length);
+ destPos += buf.length;
+ }
+ return destPos;
+ }
+
+ public void updateDigest(MessageDigest digest) {
+ for (byte[] buf : buffer) {
+ digest.update(buf);
+ }
+ }
+
+ public ArrayList rawList() {
+ return buffer;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/BytesSink.java b/ripple-core/src/main/java/com/ripple/core/serialized/BytesSink.java
new file mode 100644
index 0000000000..7ec5c6cc5a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/BytesSink.java
@@ -0,0 +1,8 @@
+package com.ripple.core.serialized;
+
+public interface BytesSink {
+ default void add(byte aByte) {
+ add(new byte[] {aByte});
+ }
+ void add(byte[] bytes);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/MultiSink.java b/ripple-core/src/main/java/com/ripple/core/serialized/MultiSink.java
new file mode 100644
index 0000000000..02de05e311
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/MultiSink.java
@@ -0,0 +1,16 @@
+package com.ripple.core.serialized;
+
+public class MultiSink implements BytesSink {
+ final private BytesSink[] sinks;
+ public MultiSink(BytesSink... sinks) {
+ this.sinks = sinks;
+ }
+ @Override
+ public void add(byte b) {
+ for (BytesSink sink : sinks) sink.add(b);
+ }
+ @Override
+ public void add(byte[] b) {
+ for (BytesSink sink : sinks) sink.add(b);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/SerializedType.java b/ripple-core/src/main/java/com/ripple/core/serialized/SerializedType.java
new file mode 100644
index 0000000000..1b5263e935
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/SerializedType.java
@@ -0,0 +1,11 @@
+package com.ripple.core.serialized;
+
+import com.ripple.core.fields.Type;
+
+public interface SerializedType {
+ Object toJSON();
+ byte[] toBytes();
+ String toHex();
+ void toBytesSink(BytesSink to);
+ Type type();
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/StreamBinaryParser.java b/ripple-core/src/main/java/com/ripple/core/serialized/StreamBinaryParser.java
new file mode 100644
index 0000000000..b3f964c31d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/StreamBinaryParser.java
@@ -0,0 +1,81 @@
+package com.ripple.core.serialized;
+
+import java.io.*;
+import java.util.zip.GZIPInputStream;
+
+public class StreamBinaryParser extends BinaryParser {
+ final BufferedInputStream stream;
+
+ public StreamBinaryParser(InputStream stream, long size) {
+ super((int) size);
+ this.stream = new BufferedInputStream(stream);
+ }
+
+ private static boolean isGZip(File fio) {
+ return fio.getName().endsWith("gz");
+ }
+ private static int getUncompressedSize(File fio) {
+ if (isGZip(fio)) {
+ int val;
+ try {
+ RandomAccessFile raf = new RandomAccessFile(fio, "r");
+ raf.seek(raf.length() - 4);
+ int b4 = raf.read();
+ int b3 = raf.read();
+ int b2 = raf.read();
+ int b1 = raf.read();
+ val = (b1 << 24) | (b2 << 16) + (b3 << 8) + b4;
+ raf.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return val;
+ } else {
+ return (int) fio.length();
+ }
+ }
+
+ public void skip(int n) {
+ try {
+ long skipped = stream.skip(n);
+ if (skipped != n) {
+ throw new RuntimeException("Expected to skip more bytes");
+ }
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ public byte readOne() {
+ return read(1)[0];
+ }
+ public byte[] read(int n) {
+ byte[] ret = new byte[n];
+ try {
+ int read = stream.read(ret);
+ if (read != n) {
+ throw new RuntimeException("Expected to read more bytes");
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ cursor += n;
+ return ret;
+ }
+ public static StreamBinaryParser fromFile(String path) {
+ try {
+ File f = new File(path);
+ FileInputStream fstream = new FileInputStream(path);
+ InputStream stream = fstream;
+ long s = fstream.getChannel().size();
+
+ if (isGZip(f)) {
+ s = getUncompressedSize(f);
+ stream = new GZIPInputStream(fstream);
+ }
+ return new StreamBinaryParser(stream, s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/StreamSink.java b/ripple-core/src/main/java/com/ripple/core/serialized/StreamSink.java
new file mode 100644
index 0000000000..34fe16d377
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/StreamSink.java
@@ -0,0 +1,31 @@
+package com.ripple.core.serialized;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class StreamSink implements BytesSink {
+ OutputStream out;
+
+ public StreamSink(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void add(byte aByte) {
+ try {
+ out.write(aByte);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void add(byte[] bytes) {
+ try {
+ out.write(bytes);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
+
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/TypeTranslator.java b/ripple-core/src/main/java/com/ripple/core/serialized/TypeTranslator.java
new file mode 100644
index 0000000000..4bd988338b
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/TypeTranslator.java
@@ -0,0 +1,151 @@
+
+package com.ripple.core.serialized;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.runtime.Value;
+import com.ripple.encodings.common.B16;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * @param The SerializedType class
+ * TODO, this should only really have methods that each class over-rides
+ * it's currently pretty NASTY
+ */
+public abstract class TypeTranslator {
+
+ @SuppressWarnings("unchecked")
+ public T fromValue(Object object) {
+ switch (Value.typeOf(object)) {
+ case STRING:
+ return fromString((String) object);
+ case JACKSON_ARRAY:
+ return fromJacksonArray((ArrayNode) object);
+ case JACKSON_BINARY:
+ throw new IllegalStateException("cant handle jackson binary");
+ case JACKSON_BOOLEAN:
+ return fromBoolean(((JsonNode) object).asBoolean());
+ case JACKSON_MISSING:
+ throw new IllegalStateException("cant create from missing");
+ case JACKSON_NULL:
+ throw new IllegalStateException("cant create from null");
+ case JACKSON_NUMBER:
+ JsonNode node = (JsonNode) object;
+ if (node.isLong()) {
+ return fromLong(node.asLong());
+ } else if (node.isInt()) {
+ return fromInteger(node.asInt());
+ } else if (node.isDouble()) {
+ return fromDouble(node.asDouble());
+ } else if (node.isFloat()) {
+ return fromDouble(node.floatValue());
+ }
+ throw new IllegalStateException("cant create from null");
+ case JACKSON_OBJECT:
+ return fromJacksonObject((ObjectNode) object);
+ case JACKSON_POJO:
+ throw new IllegalStateException("cant handle POJO");
+ case JACKSON_STRING:
+ return fromString(((JsonNode) object).asText());
+ case DOUBLE:
+ return fromDouble((Double) object);
+ case INTEGER:
+ return fromInteger((Integer) object);
+ case LONG:
+ return fromLong((Long) object);
+ case BOOLEAN:
+ return fromBoolean((Boolean) object);
+ case JSON_ARRAY:
+ return fromJSONArray((JSONArray) object);
+ case JSON_OBJECT:
+ return fromJSONObject((JSONObject) object);
+ case BYTE_ARRAY:
+ return fromBytes((byte[]) object);
+ case UNKNOWN:
+ default:
+ return (T) object;
+ }
+
+ }
+
+ protected T fromJacksonObject(ObjectNode object) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected T fromJacksonArray(ArrayNode node) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String toString(T obj) {
+ return obj.toString();
+ }
+
+ protected T fromJSONObject(JSONObject jsonObject) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected T fromJSONArray(JSONArray jsonArray) {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ protected T fromBoolean(boolean aBoolean) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected T fromLong(long aLong) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected T fromInteger(int integer) {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ protected T fromDouble(double aDouble) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected T fromString(String value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @param hint Using a boxed integer, allowing null for no hint
+ */
+ public abstract T fromParser(BinaryParser parser, Integer hint);
+
+ public T fromParser(BinaryParser parser) {
+ return fromParser(parser, null);
+ }
+
+ public T fromBytes(byte[] b) {
+ return fromParser(new BinaryParser(b));
+ }
+
+ public T fromHex(String hex) {
+ return fromBytes(B16.decode(hex));
+ }
+
+ public Object toJSON(T obj) {
+ return obj.toJSON();
+ }
+
+ public void toBytesSink(T obj, BytesSink to) {
+ obj.toBytesSink(to);
+ }
+
+ public byte[] toBytes(T obj) {
+ BytesList to = new BytesList();
+ toBytesSink(obj, to);
+ return to.bytes();
+ }
+
+ public String toHex(T obj) {
+ BytesList to = new BytesList();
+ toBytesSink(obj, to);
+ return to.bytesHex();
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/enums/EngineResult.java b/ripple-core/src/main/java/com/ripple/core/serialized/enums/EngineResult.java
new file mode 100644
index 0000000000..dafd5d761a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/enums/EngineResult.java
@@ -0,0 +1,258 @@
+package com.ripple.core.serialized.enums;
+
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+
+import java.util.TreeMap;
+
+class Holder {
+ public static int ords = 0;
+}
+
+public enum EngineResult implements SerializedType {
+ telLOCAL_ERROR(-399, "Local failure."),
+ telBAD_DOMAIN("Domain too long."),
+ telBAD_PATH_COUNT("Malformed: Too many paths."),
+ telBAD_PUBLIC_KEY("Public key too long."),
+ telFAILED_PROCESSING("Failed to correctly process transaction."),
+ telINSUF_FEE_P("Fee insufficient."),
+ telNO_DST_PARTIAL("Partial payment to create account not allowed."),
+
+ telCAN_NOT_QUEUE("Can not queue at this time."),
+ telCAN_NOT_QUEUE_BALANCE("Can not queue at this time: insufficient balance to pay all queued fees."),
+ telCAN_NOT_QUEUE_BLOCKS("Can not queue at this time: would block later queued transaction(s)."),
+
+ telCAN_NOT_QUEUE_BLOCKED("Can not queue at this time: blocking transaction in queue."),
+ telCAN_NOT_QUEUE_FEE("Can not queue at this time: fee insufficient to replace queued transaction."),
+ telCAN_NOT_QUEUE_FULL("Can not queue at this time: queue is full."),
+
+ temMALFORMED(-299, "Malformed transaction."),
+ temBAD_AMOUNT("Can only send positive amounts."),
+ temBAD_CURRENCY("Malformed: Bad currency."),
+ temBAD_EXPIRATION("Malformed: Bad expiration."),
+ temBAD_FEE("Invalid fee, negative or not XRP."),
+ temBAD_ISSUER("Malformed: Bad issuer."),
+ temBAD_LIMIT("Limits must be non-negative."),
+ temBAD_OFFER("Malformed: Bad offer."),
+ temBAD_PATH("Malformed: Bad path."),
+ temBAD_PATH_LOOP("Malformed: Loop in path."),
+ temBAD_SEND_XRP_LIMIT("Malformed: Limit quality is not allowed for XRP to XRP."),
+ temBAD_SEND_XRP_MAX("Malformed: Send max is not allowed for XRP to XRP."),
+ temBAD_SEND_XRP_NO_DIRECT("Malformed: No Ripple direct is not allowed for XRP to XRP."),
+ temBAD_SEND_XRP_PARTIAL("Malformed: Partial payment is not allowed for XRP to XRP."),
+ temBAD_SEND_XRP_PATHS("Malformed: Paths are not allowed for XRP to XRP."),
+ temBAD_SEQUENCE("Malformed: Sequence is not in the past."),
+ temBAD_SIGNATURE("Malformed: Bad signature."),
+ temBAD_SRC_ACCOUNT("Malformed: Bad source account."),
+ temBAD_TRANSFER_RATE("Malformed: Transfer rate must be >= 1.0 and <= 2.0"),
+ temDST_IS_SRC("Destination may not be source."),
+ temDST_NEEDED("Destination not specified."),
+ temINVALID("The transaction is ill-formed."),
+ temINVALID_FLAG("The transaction has an invalid flag."),
+ temREDUNDANT("Sends same currency to self."),
+ temRIPPLE_EMPTY("PathSet with no paths."),
+ temDISABLED("The transaction requires logic that is currently disabled."),
+ temBAD_SIGNER("Malformed: No signer may duplicate account or other signers."),
+ temBAD_QUORUM("Malformed: Quorum is unreachable."),
+ temBAD_WEIGHT("Malformed: Weight must be a positive value."),
+ temBAD_TICK_SIZE("Malformed: Tick size out of range."),
+ temINVALID_ACCOUNT_ID("Malformed: A field contains an invalid account ID."),
+ temCANNOT_PREAUTH_SELF("Malformed: An account may not preauthorize itself."),
+ temUNCERTAIN("In process of determining result. Never returned."),
+ temUNKNOWN(-266, "The transaction requires logic that is not implemented yet."),
+
+ tefFAILURE(-199, "Failed to apply."),
+ tefALREADY("The exact transaction was already in this ledger."),
+ tefBAD_ADD_AUTH("Not authorized to add account."),
+ tefBAD_AUTH("Transaction's public key is not authorized."),
+ tefBAD_LEDGER("Ledger in unexpected state."),
+ tefCREATED("Can't add an already created account."),
+ tefEXCEPTION("Unexpected program state."),
+ tefINTERNAL("Internal error."),
+ tefNO_AUTH_REQUIRED("Auth is not required."),
+ tefPAST_SEQ("This sequence number has already passed."),
+ tefWRONG_PRIOR("This previous transaction does not match."),
+ tefMASTER_DISABLED("Master key is disabled."),
+ tefMAX_LEDGER("Ledger sequence too high."),
+ tefBAD_SIGNATURE("A signature is provided for a non-signer."),
+ tefBAD_QUORUM("Signatures provided do not meet the quorum."),
+ tefNOT_MULTI_SIGNING("Account has no appropriate list of multi-signers."),
+ tefBAD_AUTH_MASTER("Auth for unclaimed account needs correct master key."),
+ tefINVARIANT_FAILED("Fee claim violated invariants for the transaction."),
+
+
+
+
+ terRETRY(-99, "Retry transaction."),
+ terFUNDS_SPENT("Can't set password, password set funds already spent."),
+ terINSUF_FEE_B("Account balance can't pay fee."),
+ terNO_ACCOUNT("The source account does not exist."),
+ terNO_AUTH("Not authorized to hold IOUs."),
+ terNO_LINE("No such line."),
+ terOWNERS("Non-zero owner count."),
+ terPRE_SEQ("Missing/inapplicable prior transaction."),
+ terLAST("Process last."),
+ terNO_RIPPLE("Path does not permit rippling."),
+ terQUEUED("Held until escalated fee drops."),
+
+ tesSUCCESS(0, "The transaction was applied. Only final in a validated ledger."),
+
+ tecCLAIM(100, "Fee claimed. Sequence used. No action."),
+ tecPATH_PARTIAL(101, "Path could not send full amount."),
+ tecUNFUNDED_ADD(102, "Insufficient XRP balance for WalletAdd."),
+ tecUNFUNDED_OFFER(103, "Insufficient balance to fund created offer."),
+ tecUNFUNDED_PAYMENT(104, "Insufficient XRP balance to send."),
+ tecFAILED_PROCESSING(105, "Failed to correctly process transaction."),
+ tecDIR_FULL(121, "Can not add entry to full directory."),
+ tecINSUF_RESERVE_LINE(122, "Insufficient reserve to add trust line."),
+ tecINSUF_RESERVE_OFFER(123, "Insufficient reserve to create offer."),
+ tecNO_DST(124, "Destination does not exist. Send XRP to create it."),
+ tecNO_DST_INSUF_XRP(125, "Destination does not exist. Too little XRP sent to create it."),
+ tecNO_LINE_INSUF_RESERVE(126, "No such line. Too little reserve to create it."),
+ tecNO_LINE_REDUNDANT(127, "Can't set non-existent line to default."),
+ tecPATH_DRY(128, "Path could not send partial amount."),
+ tecUNFUNDED(129, "One of _ADD, _OFFER, or _SEND. Deprecated."),
+ tecNO_ALTERNATIVE_KEY(130, "The operation would remove the ability to sign transactions with the account."),
+ tecNO_REGULAR_KEY(131, "Regular key is not set."),
+ tecOWNERS(132, "Non-zero owner count."),
+ tecNO_ISSUER(133, "Issuer account does not exist."),
+ tecNO_AUTH(134, "Not authorized to hold asset."),
+ tecNO_LINE(135, "No such line."),
+ tecINSUFF_FEE(136, "Insufficient balance to pay fee."),
+ tecFROZEN(137, "Asset is frozen."),
+ tecNO_TARGET(138, "Target account does not exist."),
+ tecNO_PERMISSION(139, "No permission to perform requested operation."),
+ tecNO_ENTRY(140, "No matching entry found."),
+ tecINSUFFICIENT_RESERVE(141, "Insufficient reserve to complete requested operation."),
+ tecNEED_MASTER_KEY(142, "The operation requires the use of the Master Key."),
+ tecDST_TAG_NEEDED(143, "A destination tag is required."),
+ tecINTERNAL(144, "An internal error has occurred during processing."),
+ tecOVERSIZE(145, "Object exceeded serialization limits."),
+ tecCRYPTOCONDITION_ERROR(146, "Malformed, invalid, or mismatched conditional or fulfillment."),
+ tecINVARIANT_FAILED(147, "One or more invariants for the transaction were not satisfied."),
+ tecEXPIRED(148, "Expiration time is passed."),
+ tecDUPLICATE(149, "Ledger object already exists."),
+ tecKILLED(150, "FillOrKill offer killed."),
+ ;
+
+
+ public int asInteger() {
+ return ord;
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt8;
+ }
+
+ private static int ords = 0;
+ int ord;
+ public String human;
+ EngineResult class_ = null;
+
+ EngineResult(Integer i, String s) {
+ human = s;
+ if (i == null) {
+ i = ++Holder.ords;
+ } else {
+ Holder.ords = i;
+ }
+ ord = i;
+ }
+
+ EngineResult(String s) {
+ this(null, s);
+ }
+
+ private static TreeMap byCode;
+
+ static {
+ byCode = new TreeMap<>();
+ for (EngineResult ter : EngineResult.values()) {
+ byCode.put(ter.ord, ter);
+ }
+ }
+
+ public static EngineResult fromNumber(Number i) {
+ return byCode.get(i.intValue());
+ }
+
+
+ /*Serialized Type implementation*/
+ @Override
+ public byte[] toBytes() {
+ return new byte[]{(byte) ord};
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ to.add((byte) ord);
+ }
+
+ @Override
+ public Object toJSON() {
+ return toString();
+ }
+
+ @Override
+ public String toHex() {
+ return B16.encode(toBytes());
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public EngineResult fromParser(BinaryParser parser, Integer hint) {
+ return fromInteger(parser.readOneInt());
+ }
+
+ @Override
+ public EngineResult fromString(String value) {
+ return EngineResult.valueOf(value);
+ }
+
+ @Override
+ public EngineResult fromInteger(int integer) {
+ return fromNumber(integer);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static Translator translate = new Translator();
+
+ // Result Classes
+ public static EngineResult resultClass(EngineResult result) {
+ if (result.ord >= telLOCAL_ERROR.ord && result.ord < temMALFORMED.ord) {
+ return telLOCAL_ERROR;
+ }
+ if (result.ord >= temMALFORMED.ord && result.ord < tefFAILURE.ord) {
+ return temMALFORMED;
+ }
+ if (result.ord >= tefFAILURE.ord && result.ord < terRETRY.ord) {
+ return tefFAILURE;
+ }
+ if (result.ord >= terRETRY.ord && result.ord < tesSUCCESS.ord) {
+ return terRETRY;
+ }
+ if (result.ord >= tesSUCCESS.ord && result.ord < tecCLAIM.ord) {
+ return tesSUCCESS;
+ }
+ return tecCLAIM;
+ }
+
+ public EngineResult resultClass() {
+ return class_;
+ }
+
+ static {
+ for (EngineResult engineResult : EngineResult.values()) {
+ engineResult.class_ = resultClass(engineResult);
+ }
+ }
+
+}
+
+
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/enums/LedgerEntryType.java b/ripple-core/src/main/java/com/ripple/core/serialized/enums/LedgerEntryType.java
new file mode 100644
index 0000000000..cba003d2d9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/enums/LedgerEntryType.java
@@ -0,0 +1,101 @@
+package com.ripple.core.serialized.enums;
+
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+public enum LedgerEntryType implements SerializedType {
+ // Invalid (-1),
+ AccountRoot('a'),
+ DirectoryNode('d'),
+ // GeneratorMap ('g'),
+ RippleState('r'),
+ Escrow('u'),
+ // Nickname ('n'), // deprecated
+ Offer('o'),
+ // Contract ('c'),
+ LedgerHashes('h'),
+ Amendments('f'),
+ FeeSettings('s'),
+ Ticket('T'),
+ SignerList('S'),
+ PayChannel('x'),
+ Check('C'),
+ DepositPreauth('p');
+
+ final int ord;
+
+ LedgerEntryType(int i) {
+ ord = i;
+ }
+
+ static private Map byCode = new TreeMap<>();
+
+ static {
+ for (Object a : LedgerEntryType.values()) {
+ LedgerEntryType f = (LedgerEntryType) a;
+ byCode.put(f.ord, f);
+ }
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt16;
+ }
+
+ public static LedgerEntryType fromNumber(Number i) {
+ return byCode.get(i.intValue());
+ }
+
+ public Integer asInteger() {
+ return ord;
+ }
+
+ // SeralizedType interface
+ @Override
+ public byte[] toBytes() {
+ return new byte[]{(byte) ((ord >>> 8) & 0xFF), (byte) (ord & 0xFF)};
+ }
+
+ @Override
+ public Object toJSON() {
+ return toString();
+ }
+
+ @Override
+ public String toHex() {
+ return B16.encode(toBytes());
+ }
+
+ @Override
+ public void toBytesSink(BytesSink to) {
+ to.add(toBytes());
+ }
+
+ public static class Translator extends TypeTranslator {
+ @Override
+ public LedgerEntryType fromParser(BinaryParser parser, Integer hint) {
+ return fromNumber(parser.readOneInt() << 8 | parser.readOneInt());
+ }
+
+ @Override
+ public LedgerEntryType fromInteger(int integer) {
+ return fromNumber(integer);
+ }
+
+ @Override
+ public LedgerEntryType fromString(String value) {
+ return LedgerEntryType.valueOf(value);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static Translator translate = new Translator();
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/serialized/enums/TransactionType.java b/ripple-core/src/main/java/com/ripple/core/serialized/enums/TransactionType.java
new file mode 100644
index 0000000000..7c98c3da07
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/serialized/enums/TransactionType.java
@@ -0,0 +1,103 @@
+package com.ripple.core.serialized.enums;
+
+import com.ripple.core.fields.Type;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.TypeTranslator;
+import com.ripple.encodings.common.B16;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+public enum TransactionType implements SerializedType {
+ // Invalid (-1),
+ Payment (0),
+ EscrowCreate (1), // open
+ EscrowFinish (2),
+ AccountSet (3), // open,
+ EscrowCancel (4),
+ SetRegularKey(5),
+ // unused1 (6), // open
+ OfferCreate (7),
+ OfferCancel (8),
+ // unused2(9),
+ TicketCreate(10),
+ TicketCancel(11),
+ SignerListSet(12),
+ PaymentChannelCreate(13),
+ PaymentChannelFund(14),
+ PaymentChannelClaim(15),
+ CheckCreate(16),
+ CheckCash(17),
+ CheckCancel(18),
+ DepositPreauth(19),
+ TrustSet (20),
+ EnableAmendment(100),
+ SetFee(101);
+
+ public int asInteger() {
+ return ord;
+ }
+
+ final int ord;
+ TransactionType(int i) {
+ ord = i;
+ }
+
+ @Override
+ public Type type() {
+ return Type.UInt16;
+ }
+
+ static private Map byCode = new TreeMap<>();
+ static {
+ for (Object a : TransactionType.values()) {
+ TransactionType f = (TransactionType) a;
+ byCode.put(f.ord, f);
+ }
+ }
+
+ static public TransactionType fromNumber(Number i) {
+ return byCode.get(i.intValue());
+ }
+
+ // SeralizedType interface
+ @Override
+ public byte[] toBytes() {
+ // TODO: bytes
+ return new byte[]{(byte) (ord >> 8), (byte) (ord & 0xFF)};
+ }
+ @Override
+ public Object toJSON() {
+ return toString();
+ }
+ @Override
+ public String toHex() {
+ return B16.encode(toBytes());
+ }
+ @Override
+ public void toBytesSink(BytesSink to) {
+ to.add(toBytes());
+ }
+ public static class Translator extends TypeTranslator {
+ @Override
+ public TransactionType fromParser(BinaryParser parser, Integer hint) {
+ return fromNumber(parser.readOneInt() << 8 | parser.readOneInt());
+ }
+
+ @Override
+ public TransactionType fromInteger(int integer) {
+ return fromNumber(integer);
+ }
+
+ @Override
+ public TransactionType fromString(String value) {
+ return TransactionType.valueOf(value);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static Translator translate = new Translator();
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/generic/Validation.java b/ripple-core/src/main/java/com/ripple/core/types/known/generic/Validation.java
new file mode 100644
index 0000000000..f7efd08ed2
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/generic/Validation.java
@@ -0,0 +1,15 @@
+package com.ripple.core.types.known.generic;
+
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+
+public class Validation extends STObject {
+ public static boolean isValidation(STObject source) {
+ return source.has(UInt32.LedgerSequence) &&
+ source.has(UInt32.SigningTime) &&
+ source.has(Hash256.LedgerHash) &&
+ source.has(Blob.Signature);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerEntry.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerEntry.java
new file mode 100644
index 0000000000..c5dc3f7f3d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerEntry.java
@@ -0,0 +1,59 @@
+package com.ripple.core.types.known.sle;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.Field;
+import com.ripple.core.formats.LEFormat;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.entries.AccountRoot;
+import com.ripple.core.types.known.sle.entries.DirectoryNode;
+import com.ripple.core.types.known.sle.entries.Offer;
+import com.ripple.core.types.known.sle.entries.RippleState;
+
+import java.util.TreeSet;
+
+public class LedgerEntry extends STObject {
+ public LedgerEntry(LedgerEntryType type) {
+ setFormat(LEFormat.formats.get(type));
+ put(Field.LedgerEntryType, type);
+ }
+
+ public LedgerEntryType ledgerEntryType() {return ledgerEntryType(this);}
+ public Hash256 index() { return get(Hash256.index); }
+ public UInt32 flags() {return get(UInt32.Flags);}
+ public Hash256 ledgerIndex() {return get(Hash256.LedgerIndex);}
+
+ public void flags(UInt32 val) {put(Field.Flags, val);}
+ public void ledgerIndex(Hash256 val) {put(Field.LedgerIndex, val);}
+
+ public boolean hasLedgerIndex() {return has(Hash256.LedgerIndex);}
+
+ public TreeSet owners() {
+ TreeSet owners = new TreeSet<>();
+
+ if (has(Field.LowLimit)) {
+ owners.add(get(Amount.LowLimit).issuer());
+ }
+ if (has(Field.HighLimit)) {
+ owners.add(get(Amount.HighLimit).issuer());
+ }
+ if (has(Field.Account)) {
+ owners.add(get(AccountID.Account));
+ }
+
+ return owners;
+ }
+
+ public void index(Hash256 index) {
+ put(Hash256.index, index);
+ }
+
+ public void setDefaults() {
+ if (flags() == null) {
+ flags(UInt32.ZERO);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerHashes.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerHashes.java
new file mode 100644
index 0000000000..9b9086cd72
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/LedgerHashes.java
@@ -0,0 +1,32 @@
+package com.ripple.core.types.known.sle;
+
+import com.ripple.core.coretypes.Vector256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+public class LedgerHashes extends LedgerEntry {
+ public LedgerHashes() {
+ super(LedgerEntryType.LedgerHashes);
+ }
+
+ public Vector256 hashes() {
+ return get(Vector256.Hashes);
+ }
+
+ public void hashes(Vector256 hashes) {
+ put(Vector256.Hashes, hashes);
+ }
+
+ public UInt32 lastLedgerSequence() {
+ return get(UInt32.LastLedgerSequence);
+ }
+
+ public UInt32 firstLedgerSequence() {return get(UInt32.FirstLedgerSequence);}
+
+ public void firstLedgerSequence(UInt32 val) { put(UInt32.FirstLedgerSequence, val);}
+
+ public void lastLedgerSequence(UInt32 val) { put(UInt32.LastLedgerSequence, val);}
+
+ public boolean hasFirstLedgerSequence() {return has(UInt32.FirstLedgerSequence);}
+ public boolean hasLastLedgerSequence() {return has(UInt32.LastLedgerSequence);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/ThreadedLedgerEntry.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/ThreadedLedgerEntry.java
new file mode 100644
index 0000000000..3739109d97
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/ThreadedLedgerEntry.java
@@ -0,0 +1,18 @@
+package com.ripple.core.types.known.sle;
+
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+// this class has a PreviousTxnID and PreviousTxnLgrSeq
+abstract public class ThreadedLedgerEntry extends LedgerEntry {
+ public ThreadedLedgerEntry(LedgerEntryType type) {
+ super(type);
+ }
+ public UInt32 previousTxnLgrSeq() {return get(UInt32.PreviousTxnLgrSeq);}
+ public Hash256 previousTxnID() {return get(Hash256.PreviousTxnID);}
+ public void previousTxnLgrSeq(UInt32 val) {put(Field.PreviousTxnLgrSeq, val);}
+ public void previousTxnID(Hash256 val) {put(Field.PreviousTxnID, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/AccountRoot.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/AccountRoot.java
new file mode 100644
index 0000000000..22f26def99
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/AccountRoot.java
@@ -0,0 +1,71 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.hash.Hash128;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt8;
+import com.ripple.core.enums.LedgerFlag;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.ThreadedLedgerEntry;
+
+public class AccountRoot extends ThreadedLedgerEntry {
+ public AccountRoot() {
+ super(LedgerEntryType.AccountRoot);
+ }
+
+ public UInt32 sequence() {return get(UInt32.Sequence);}
+ public UInt32 transferRate() {return get(UInt32.TransferRate);}
+ public UInt32 walletSize() {return get(UInt32.WalletSize);}
+ public UInt32 ownerCount() {return get(UInt32.OwnerCount);}
+ public Hash128 emailHash() {return get(Hash128.EmailHash);}
+ public Hash256 walletLocator() {return get(Hash256.WalletLocator);}
+ public Amount balance() {return get(Amount.Balance);}
+ public Blob messageKey() {return get(Blob.MessageKey);}
+ public Blob domain() {return get(Blob.Domain);}
+ public AccountID account() {return get(AccountID.Account);}
+ public AccountID regularKey() {return get(AccountID.RegularKey);}
+
+ public void sequence(UInt32 val) {put(Field.Sequence, val);}
+ public void transferRate(UInt32 val) {put(Field.TransferRate, val);}
+ public void walletSize(UInt32 val) {put(Field.WalletSize, val);}
+ public void ownerCount(UInt32 val) {put(Field.OwnerCount, val);}
+ public void emailHash(Hash128 val) {put(Field.EmailHash, val);}
+ public void walletLocator(Hash256 val) {put(Field.WalletLocator, val);}
+ public void balance(Amount val) {put(Field.Balance, val);}
+ public void messageKey(Blob val) {put(Field.MessageKey, val);}
+ public void domain(Blob val) {put(Field.Domain, val);}
+ public void account(AccountID val) {put(Field.Account, val);}
+ public void regularKey(AccountID val) {put(Field.RegularKey, val);}
+
+ public boolean requiresAuth() {
+ return flags().testBit(LedgerFlag.RequireAuth);
+ }
+
+ public boolean hasAccountTxnID() {return has(Hash256.AccountTxnID);}
+ public boolean hasDomain() {return has(Blob.Domain);}
+ public boolean hasEmailHash() {return has(Hash128.EmailHash);}
+ public boolean hasMessageKey() {return has(Blob.MessageKey);}
+ public boolean hasRegularKey() {return has(AccountID.RegularKey);}
+ public boolean hasTickSize() {return has(UInt8.TickSize);}
+ public boolean hasTransferRate() {return has(UInt32.TransferRate);}
+ public boolean hasWalletLocator() {return has(Hash256.WalletLocator);}
+ public boolean hasWalletSize() {return has(UInt32.WalletSize);}
+
+ public Hash256 accountTxnID() {return get(Hash256.AccountTxnID);}
+ public UInt8 tickSize() {return get(UInt8.TickSize);}
+
+ public void accountTxnID(Hash256 val) { put(Hash256.AccountTxnID, val);}
+ public void tickSize(UInt8 val) { put(UInt8.TickSize, val);}
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+ if (ownerCount() == null) {
+ ownerCount(new UInt32(0));
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Amendments.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Amendments.java
new file mode 100644
index 0000000000..8b580af8ef
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Amendments.java
@@ -0,0 +1,22 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.STArray;
+import com.ripple.core.coretypes.Vector256;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+public class Amendments extends LedgerEntry {
+ public Amendments() {
+ super(LedgerEntryType.Amendments);
+ }
+
+ public STArray majorities() {return get(STArray.Majorities);}
+ public Vector256 amendments() {return get(Vector256.Amendments);}
+
+ public void amendments(Vector256 val) { put(Vector256.Amendments, val);}
+ public void majorities(STArray val) { put(STArray.Majorities, val);}
+
+ public boolean hasAmendments() {return has(Vector256.Amendments);}
+ public boolean hasMajorities() {return has(STArray.Majorities);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Check.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Check.java
new file mode 100644
index 0000000000..310232b1dd
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Check.java
@@ -0,0 +1,40 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+public class Check extends IndexedLedgerEntry {
+ public Check() {
+ super(LedgerEntryType.Check);
+ }
+
+ public boolean hasSourceTag() {return has(UInt32.SourceTag);}
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasInvoiceID() {return has(Hash256.InvoiceID);}
+
+ public UInt32 sourceTag() {return get(UInt32.SourceTag);}
+ public UInt32 sequence() {return get(UInt32.Sequence);}
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+ public UInt64 destinationNode() {return get(UInt64.DestinationNode);}
+ public Hash256 invoiceID() {return get(Hash256.InvoiceID);}
+ public Amount sendMax() {return get(Amount.SendMax);}
+ public AccountID account() {return get(AccountID.Account);}
+ public AccountID destination() {return get(AccountID.Destination);}
+
+ public void sourceTag(UInt32 val) { put(UInt32.SourceTag, val);}
+ public void sequence(UInt32 val) { put(UInt32.Sequence, val);}
+ public void expiration(UInt32 val) { put(UInt32.Expiration, val);}
+ public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);}
+ public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);}
+ public void destinationNode(UInt64 val) { put(UInt64.DestinationNode, val);}
+ public void invoiceID(Hash256 val) { put(Hash256.InvoiceID, val);}
+ public void sendMax(Amount val) { put(Amount.SendMax, val);}
+ public void destination(AccountID val) { put(AccountID.Destination, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DepositPreauthLe.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DepositPreauthLe.java
new file mode 100644
index 0000000000..7f995ecc1c
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DepositPreauthLe.java
@@ -0,0 +1,18 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+public class DepositPreauthLe extends IndexedLedgerEntry {
+ public DepositPreauthLe() {
+ super(LedgerEntryType.DepositPreauth);
+ }
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+ public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);}
+
+ public AccountID account() {return get(AccountID.Account);}
+
+ public AccountID authorize() {return get(AccountID.Authorize);}
+ public void authorize(AccountID val) { put(AccountID.Authorize, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DirectoryNode.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DirectoryNode.java
new file mode 100644
index 0000000000..f46a7d7973
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/DirectoryNode.java
@@ -0,0 +1,88 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Currency;
+import com.ripple.core.coretypes.Vector256;
+import com.ripple.core.coretypes.hash.Hash160;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+public class DirectoryNode extends LedgerEntry {
+ public DirectoryNode() {
+ super(LedgerEntryType.DirectoryNode);
+ }
+
+ public UInt64 indexNext() {return get(UInt64.IndexNext);}
+ public UInt64 indexPrevious() {return get(UInt64.IndexPrevious);}
+ public UInt64 exchangeRate() {return get(UInt64.ExchangeRate);}
+ public Hash256 rootIndex() {return get(Hash256.RootIndex);}
+ public AccountID owner() {return get(AccountID.Owner);}
+ public Hash160 takerPaysCurrency() {return get(Hash160.TakerPaysCurrency);}
+ public Hash160 takerPaysIssuer() {return get(Hash160.TakerPaysIssuer);}
+ public Hash160 takerGetsCurrency() {return get(Hash160.TakerGetsCurrency);}
+ public Hash160 takerGetsIssuer() {return get(Hash160.TakerGetsIssuer);}
+ public Vector256 indexes() {return get(Vector256.Indexes);}
+ public void indexNext(UInt64 val) {put(Field.IndexNext, val);}
+ public void indexPrevious(UInt64 val) {put(Field.IndexPrevious, val);}
+ public void exchangeRate(UInt64 val) {put(Field.ExchangeRate, val);}
+ public void rootIndex(Hash256 val) {put(Field.RootIndex, val);}
+ public void owner(AccountID val) {put(Field.Owner, val);}
+ public void takerPaysCurrency(Hash160 val) {put(Field.TakerPaysCurrency, val);}
+ public void takerPaysIssuer(Hash160 val) {put(Field.TakerPaysIssuer, val);}
+ public void takerGetsCurrency(Hash160 val) {put(Field.TakerGetsCurrency, val);}
+ public void takerGetsIssuer(Hash160 val) {put(Field.TakerGetsIssuer, val);}
+ public void indexes(Vector256 val) {put(Field.Indexes, val);}
+
+ public boolean hasIndexNext() {return has(UInt64.IndexNext);}
+ public boolean hasIndexPrevious() {return has(UInt64.IndexPrevious);}
+ public boolean hasExchangeRate() {return has(UInt64.ExchangeRate);}
+ public boolean hasOwner() {return has(AccountID.Owner);}
+ public boolean hasTakerPaysCurrency() {return has(Hash160.TakerPaysCurrency);}
+ public boolean hasTakerPaysIssuer() {return has(Hash160.TakerPaysIssuer);}
+ public boolean hasTakerGetsCurrency() {return has(Hash160.TakerGetsCurrency);}
+ public boolean hasTakerGetsIssuer() {return has(Hash160.TakerGetsIssuer);}
+
+ public Hash256 nextIndex() {
+ return Index.directoryNode(rootIndex(), indexNext());
+ }
+ public Hash256 prevIndex() {
+ return Index.directoryNode(rootIndex(), indexPrevious());
+ }
+
+ public boolean hasPreviousIndex() {
+ return hasIndexPrevious() && !indexPrevious().isZero();
+ }
+
+ public boolean hasNextIndex() {
+ return hasIndexNext() && !indexNext().isZero();
+ }
+
+ public boolean isRootIndex() {
+ return rootIndex().equals(index());
+ }
+
+ public void setExchangeDefaults() {
+ if (takerGetsCurrency() == null) {
+ takerGetsCurrency(Currency.XRP);
+ takerGetsIssuer(AccountID.XRP_ISSUER);
+ } else if (takerPaysCurrency() == null) {
+ takerPaysCurrency(Currency.XRP);
+ takerPaysIssuer(AccountID.XRP_ISSUER);
+ }
+ }
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+ if (exchangeRate() != null) {
+ setExchangeDefaults();
+ }
+ if (indexes() == null) {
+ indexes(new Vector256());
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Escrow.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Escrow.java
new file mode 100644
index 0000000000..ff822c6757
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Escrow.java
@@ -0,0 +1,74 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.tx.Transaction;
+
+import java.util.ArrayList;
+
+public class Escrow extends IndexedLedgerEntry {
+ public Escrow() {
+ super(LedgerEntryType.Escrow);
+ }
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+ if (multiParty()) {
+ if (!has(UInt64.DestinationNode)) {
+ put(UInt64.DestinationNode, UInt64.ZERO);
+ }
+ }
+ }
+
+ @Override
+ public ArrayList ownerDirectoryIndexes(Transaction nullableContext) {
+ ArrayList indexes = super.ownerDirectoryIndexes(nullableContext);
+ if (multiParty()) {
+ Hash256 destinationOwnerDir =
+ Index.ownerDirectory(destination());
+ indexes.add(Index.directoryNode(
+ destinationOwnerDir, destinationNode()));
+ }
+ return indexes;
+ }
+
+ private boolean multiParty() {
+ return !get(AccountID.Account).equals(destination());
+ }
+
+ public AccountID account() {return get(AccountID.Account);}
+ public AccountID destination() {return get(AccountID.Destination);}
+ public Amount amount() {return get(Amount.Amount);}
+ public Blob condition() {return get(Blob.Condition);}
+ public UInt32 cancelAfter() {return get(UInt32.CancelAfter);}
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public UInt32 finishAfter() {return get(UInt32.FinishAfter);}
+ public UInt32 sourceTag() {return get(UInt32.SourceTag);}
+ public UInt64 destinationNode() {return get(UInt64.DestinationNode);}
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+
+ public void amount(Amount val) { put(Amount.Amount, val);}
+ public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);}
+ public void condition(Blob val) { put(Blob.Condition, val);}
+ public void destination(AccountID val) { put(AccountID.Destination, val);}
+ public void destinationNode(UInt64 val) { put(UInt64.DestinationNode, val);}
+ public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);}
+ public void finishAfter(UInt32 val) { put(UInt32.FinishAfter, val);}
+ public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);}
+ public void sourceTag(UInt32 val) { put(UInt32.SourceTag, val);}
+
+ public boolean hasCancelAfter() {return has(UInt32.CancelAfter);}
+ public boolean hasCondition() {return has(Blob.Condition);}
+ public boolean hasDestinationNode() {return has(UInt64.DestinationNode);}
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasFinishAfter() {return has(UInt32.FinishAfter);}
+ public boolean hasSourceTag() {return has(UInt32.SourceTag);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/FeeSettings.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/FeeSettings.java
new file mode 100644
index 0000000000..ce8fca505d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/FeeSettings.java
@@ -0,0 +1,22 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+public class FeeSettings extends LedgerEntry {
+ public FeeSettings() {
+ super(LedgerEntryType.Amendments);
+ }
+ public UInt32 referenceFeeUnits() {return get(UInt32.ReferenceFeeUnits);}
+ public UInt32 reserveBase() {return get(UInt32.ReserveBase);}
+ public UInt32 reserveIncrement() {return get(UInt32.ReserveIncrement);}
+ public UInt64 baseFee() {return get(UInt64.BaseFee);}
+
+ public void baseFee(UInt64 val) { put(UInt64.BaseFee, val);}
+ public void referenceFeeUnits(UInt32 val) { put(UInt32.ReferenceFeeUnits, val);}
+ public void reserveBase(UInt32 val) { put(UInt32.ReserveBase, val);}
+ public void reserveIncrement(UInt32 val) { put(UInt32.ReserveIncrement, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IHasOwners.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IHasOwners.java
new file mode 100644
index 0000000000..93c15e2f80
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IHasOwners.java
@@ -0,0 +1,12 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.types.known.tx.Transaction;
+
+import java.util.ArrayList;
+
+public interface IHasOwners {
+ ArrayList ownerDirectoryIndexes(Transaction nullableContext);
+ AccountID account(Transaction nullableContext);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IndexedLedgerEntry.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IndexedLedgerEntry.java
new file mode 100644
index 0000000000..37ff656961
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/IndexedLedgerEntry.java
@@ -0,0 +1,56 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.ThreadedLedgerEntry;
+import com.ripple.core.types.known.tx.Transaction;
+
+import java.util.ArrayList;
+
+public abstract class IndexedLedgerEntry extends ThreadedLedgerEntry implements IHasOwners {
+ public IndexedLedgerEntry(LedgerEntryType type) {
+ super(type);
+ }
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+ if (!has(Field.OwnerNode)) {
+ put(UInt64.OwnerNode, UInt64.ZERO);
+ }
+ }
+
+ @Override
+ public AccountID account(Transaction nullableContext) {
+ AccountID account = account();
+ if (account == null) {
+ if (nullableContext != null) {
+ account = nullableContext.account();
+ } else {
+ throw new IllegalStateException("Cant determine account for: " + prettyJSON());
+ }
+ }
+ return account;
+ }
+
+ @Override
+ public ArrayList ownerDirectoryIndexes(Transaction nullableContext) {
+ Hash256 ownerDir = Index.ownerDirectory(account(nullableContext));
+ ArrayList indexes = new ArrayList<>();
+ indexes.add(Index.directoryNode(ownerDir, ownerNode()));
+ return indexes;
+ }
+
+ private UInt64 ownerNode() {
+ return get(UInt64.OwnerNode);
+ }
+
+ private AccountID account() {
+ return get(AccountID.Account);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Offer.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Offer.java
new file mode 100644
index 0000000000..ceaa7a6732
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Offer.java
@@ -0,0 +1,219 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.*;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.ThreadedLedgerEntry;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+
+public class Offer extends ThreadedLedgerEntry {
+ public Offer() {
+ super(LedgerEntryType.Offer);
+ }
+
+ /**
+ * Use the BookDirectory field
+ *
+ * @return how much must `pay` to `get` one.
+ *
+ */
+ public BigDecimal directoryAskQuality() {
+ return Quality.fromBookDirectory(bookDirectory(),
+ takerPays().isNative(),
+ takerGets().isNative());
+ }
+
+ /**
+ * @return how much must `pay` to `get` one.
+ */
+ public BigDecimal askQuality() {
+ return takerPays().computeQuality(takerGets());
+ }
+
+ /**
+ * @return how much `get` if `pay` one.
+ */
+ public BigDecimal bidQuality() {
+ return takerGets().computeQuality(takerPays());
+ }
+
+ /**
+ *
+ * @return One of TakerGets issue, eg 1/USD/BITSTAMP or 1/EUR/SNAPSWAP
+ */
+ public Amount getsOne() {
+ return takerGets().one();
+ }
+ /**
+ *
+ * @return One of TakerPays issue, eg 1/USD/BITSTAMP or 1/EUR/SNAPSWAP
+ */
+ public Amount paysOne() {
+ return takerPays().one();
+ }
+
+ public String getPayCurrencyPair() {
+ return takerGets().currencyString() + "/" +
+ takerPays().currencyString();
+ }
+
+ // TODO: create an OfferExecution object for this
+ public STObject executed(STObject finalFields) {
+ // where `this` is an AffectedNode nodeAsPrevious
+ STObject executed = new STObject();
+ executed.put(Amount.TakerPays, finalFields.get(Amount.TakerPays).subtract(takerPays()));
+ executed.put(Amount.TakerGets, finalFields.get(Amount.TakerGets).subtract(takerGets()));
+ return executed;
+ }
+
+ public Hash256 lineIndex(Amount amt) {
+ return account().lineIndex(amt.issue());
+ }
+
+ public Hash256 fundingSource() {
+ Amount takerGets = takerGets();
+ if (account().equals(takerGets.issuer())) {
+ return null;
+ }
+ else if (takerGets.isNative()) {
+ return Index.accountRoot(account());
+ } else {
+ return lineIndex(takerGets);
+ }
+ }
+
+ public Vector256 lineIndexes() {
+ Vector256 ret = new Vector256();
+
+ Amount takerGets = takerGets();
+ for (Amount amt : new Amount[]{takerGets, takerPays()}) {
+
+ // Actually want to compare by reference here :)
+ if (amt == takerGets()) {
+ // selling own funds
+ continue;
+ }
+ if (!amt.isNative()) {
+ ret.add(lineIndex(amt));
+ }
+ }
+ return ret;
+ }
+
+ public Hash256 bookBase() {
+ return Index.bookStart(takerPays().issue(), takerGets().issue());
+ }
+
+ public boolean belongsToBook(Hash256 bookBase) {
+ byte[] baseBytes = bookBase.bytes();
+ byte[] directoryBytes = bookDirectory().bytes();
+
+ for (int i = 0; i < 24; i++) {
+ if (baseBytes[i] != directoryBytes[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean sellingOwnFunds() {
+ return account().equals(takerGets().issuer());
+ }
+
+ public Amount takerGetsFunded() {
+ return has(Field.taker_gets_funded) ? get(Amount.taker_gets_funded) : takerGets();
+ }
+ public Amount takerPaysFunded() {
+ return has(Field.taker_pays_funded) ? get(Amount.taker_pays_funded) : takerPays();
+ }
+
+ public static Comparator qualityAscending = Comparator.comparing(Offer::directoryAskQuality);
+
+ public static Iterator iterateCollection(Collection offers) {
+ final Iterator iterator = offers.iterator();
+
+ return new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public Offer next() {
+ return (Offer) iterator.next();
+ }
+
+ @Override
+ public void remove() {
+ iterator.remove();
+
+ }
+ };
+ }
+
+ public Hash256 bookNodeDirectoryIndex() {
+ return Index.directoryNode(bookDirectory(), bookNode());
+ }
+
+ public Hash256 ownerNodeDirectoryIndex() {
+ Hash256 ownerDir = Index.ownerDirectory(account());
+ return Index.directoryNode(ownerDir, ownerNode());
+ }
+
+
+ public UInt32 sequence() {return get(UInt32.Sequence);}
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public UInt64 bookNode() {return get(UInt64.BookNode);}
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+ public Hash256 bookDirectory() {return get(Hash256.BookDirectory);}
+ public Amount takerPays() {return get(Amount.TakerPays);}
+ public Amount takerGets() {return get(Amount.TakerGets);}
+ public AccountID account() {return get(AccountID.Account);}
+ public void sequence(UInt32 val) {put(Field.Sequence, val);}
+ public void expiration(UInt32 val) {put(Field.Expiration, val);}
+ public void bookNode(UInt64 val) {put(Field.BookNode, val);}
+ public void ownerNode(UInt64 val) {put(Field.OwnerNode, val);}
+ public void bookDirectory(Hash256 val) {put(Field.BookDirectory, val);}
+ public void takerPays(Amount val) {put(Field.TakerPays, val);}
+ public void takerGets(Amount val) {put(Field.TakerGets, val);}
+ public void account(AccountID val) {put(Field.Account, val);}
+
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+
+ public Hash256[] directoryIndexes() {
+ return new Hash256[]{bookNodeDirectoryIndex(), ownerNodeDirectoryIndex()};
+ }
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+ setOfferDefaults();
+ }
+
+ public void setOfferDefaults() {
+ if (bookNode() == null) {
+ bookNode(UInt64.ZERO);
+ }
+ if (ownerNode() == null) {
+ ownerNode(UInt64.ZERO);
+ }
+ }
+
+ public IssuePair issuePair() {
+ return new IssuePair(takerPays().issue(), takerGets().issue());
+ }
+
+ public Amount payToGet(Amount funded) {
+ BigDecimal quality = directoryAskQuality();
+ // Multiply by one as that will do the rounding operation we want
+ return paysOne().multiply(funded.multiply(quality));
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OfferDirectory.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OfferDirectory.java
new file mode 100644
index 0000000000..fddb7126fd
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OfferDirectory.java
@@ -0,0 +1,20 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.Issue;
+import com.ripple.core.coretypes.IssuePair;
+
+public class OfferDirectory extends DirectoryNode {
+ public IssuePair issuePair() {
+ return new IssuePair(takerPaysIssue(), takerGetsIssue());
+ }
+
+ public Issue takerGetsIssue() {
+ // TODO: remove wrapper
+ return Issue.from160s(takerGetsCurrency(), takerGetsIssuer());
+ }
+ public Issue takerPaysIssue() {
+ // TODO: remove wrapper
+ return Issue.from160s(takerPaysCurrency(), takerPaysIssuer());
+ }
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OwnerDirectory.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OwnerDirectory.java
new file mode 100644
index 0000000000..b6f784e4b8
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/OwnerDirectory.java
@@ -0,0 +1,3 @@
+package com.ripple.core.types.known.sle.entries;
+
+public class OwnerDirectory extends DirectoryNode { }
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/PayChannel.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/PayChannel.java
new file mode 100644
index 0000000000..97a2786b2a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/PayChannel.java
@@ -0,0 +1,42 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+public class PayChannel extends IndexedLedgerEntry {
+ public PayChannel() {
+ super(LedgerEntryType.PayChannel);
+ }
+
+ public AccountID account() {return get(AccountID.Account);}
+ public AccountID destination() {return get(AccountID.Destination);}
+ public Amount amount() {return get(Amount.Amount);}
+ public Amount balance() {return get(Amount.Balance);}
+ public Blob publicKey() {return get(Blob.PublicKey);}
+ public UInt32 cancelAfter() {return get(UInt32.CancelAfter);}
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public UInt32 settleDelay() {return get(UInt32.SettleDelay);}
+ public UInt32 sourceTag() {return get(UInt32.SourceTag);}
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+
+ public void amount(Amount val) { put(Amount.Amount, val);}
+ public void balance(Amount val) { put(Amount.Balance, val);}
+ public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);}
+ public void destination(AccountID val) { put(AccountID.Destination, val);}
+ public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);}
+ public void expiration(UInt32 val) { put(UInt32.Expiration, val);}
+ public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);}
+ public void publicKey(Blob val) { put(Blob.PublicKey, val);}
+ public void settleDelay(UInt32 val) { put(UInt32.SettleDelay, val);}
+ public void sourceTag(UInt32 val) { put(UInt32.SourceTag, val);}
+
+ public boolean hasCancelAfter() {return has(UInt32.CancelAfter);}
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+ public boolean hasSourceTag() {return has(UInt32.SourceTag);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/RippleState.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/RippleState.java
new file mode 100644
index 0000000000..02f9f2ad2e
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/RippleState.java
@@ -0,0 +1,224 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Currency;
+import com.ripple.core.coretypes.Issue;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.enums.LedgerFlag;
+import com.ripple.core.fields.AmountField;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.ThreadedLedgerEntry;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class RippleState extends ThreadedLedgerEntry {
+ /**
+ The RippleState is a ledger entry which roughly speaking defines the balance and
+ trust limits between two accounts.
+
+ Like all current ledger entries, it has a canonical form for hashing that
+ doesn't necessarily communicate the information in a way that is clear or
+ obvious to a human. The current json format for some ledger entries is very
+ close to the hashing format, and isn't any better.
+
+ The two accounts on the link are categorized into a low account, and a high
+ account by comparing the 160bits of their account id as a big endian unsigned
+ integer.
+
+ There is one and only one `Balance` stored, using an amount struct with a
+ neutral `issuer`. ( A uint160 with the numerical value of `1` was chosen as a
+ placeholder, as the canonical way to represent a null account )
+
+ The Balance can be negative, zero, or positive and is in terms of the Low
+ account, such that when it's positive, it defines how much credit the High
+ account has issued.
+
+ Between any account, there can be two types of balance changes, issuance and
+ redemption. A redemption is the transferal of previously issued IOUs back to the
+ owner.
+
+ This implies that for any account, that can they can make a transferal using
+ funds from two distinct balances. After making this distinction it becomes
+ nonsensical to say that an account holds negative IOUs from the opposite line.
+
+ Thus, the one Balance in a ripple state actually implies 4 distinct balances.
+
+ ```
+ {
+ ...
+ "Balance" : {
+ "currency" : "USD",
+ "issuer" : "rrrrrrrrrrrrrrrrrrrrBZbvji",
+ "value" : "100"
+ },
+ "HighLimit" : {
+ "currency" : "USD",
+ "issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
+ "value" : "500"
+ },
+ "LowLimit" : {
+ "currency" : "USD",
+ "issuer" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
+ "value" : "500"
+ },
+ }
+ ```
+
+ lowAccount = LowLimit.issuer
+ hiAccount = HighLimit.issuer
+
+ Balances:
+ lowAccount has 100/USD/highAccount
+ lowAccount has 0/USD/lowAccount on the line
+
+ highAccount has 0/USD/lowAccount
+ highAccount has -100/USD/highAccount on the line
+
+
+ * */
+
+ public RippleState() {
+ super(LedgerEntryType.RippleState);
+ }
+
+ public UInt32 highQualityIn() {return get(UInt32.HighQualityIn);}
+ public UInt32 highQualityOut() {return get(UInt32.HighQualityOut);}
+ public UInt32 lowQualityIn() {return get(UInt32.LowQualityIn);}
+ public UInt32 lowQualityOut() {return get(UInt32.LowQualityOut);}
+ public UInt64 lowNode() {return get(UInt64.LowNode);}
+ public UInt64 highNode() {return get(UInt64.HighNode);}
+ public Amount balance() {return get(Amount.Balance);}
+ public Amount lowLimit() {return get(Amount.LowLimit);}
+ public Amount highLimit() {return get(Amount.HighLimit);}
+ public void highQualityIn(UInt32 val) {put(Field.HighQualityIn, val);}
+ public void highQualityOut(UInt32 val) {put(Field.HighQualityOut, val);}
+ public void lowQualityIn(UInt32 val) {put(Field.LowQualityIn, val);}
+ public void lowQualityOut(UInt32 val) {put(Field.LowQualityOut, val);}
+ public void lowNode(UInt64 val) {put(Field.LowNode, val);}
+ public void highNode(UInt64 val) {put(Field.HighNode, val);}
+ public void balance(Amount val) {put(Field.Balance, val);}
+ public void lowLimit(Amount val) {put(Field.LowLimit, val);}
+ public void highLimit(Amount val) {put(Field.HighLimit, val);}
+
+ public boolean hasHighQualityIn() {return has(UInt32.HighQualityIn);}
+ public boolean hasHighQualityOut() {return has(UInt32.HighQualityOut);}
+ public boolean hasLowQualityIn() {return has(UInt32.LowQualityIn);}
+ public boolean hasLowQualityOut() {return has(UInt32.LowQualityOut);}
+ public boolean hasLowNode() {return has(UInt64.LowNode);}
+ public boolean hasHighNode() {return has(UInt64.HighNode);}
+
+ public AccountID lowAccount() {
+ return lowLimit().issuer();
+ }
+
+ public AccountID highAccount() {
+ return highLimit().issuer();
+ }
+
+ public List sortedAccounts() {
+ return Arrays.asList(lowAccount(), highAccount());
+ }
+
+ public AmountField limitFieldFor(AccountID source) {
+ if (lowAccount().equals(source)) {
+ return Amount.LowLimit;
+ }
+ if (highAccount().equals(source)) {
+ return Amount.HighLimit;
+ } else {
+ return null;
+ }
+ }
+
+ public boolean isFor(AccountID source) {
+ return lowAccount().equals(source) || highAccount().equals(source);
+ }
+
+ public boolean isFor(Issue issue) {
+ return isFor(issue.issuer()) && balance().currency().equals(issue.currency());
+ }
+
+ // TODO, can optimize this
+ public boolean isFor(AccountID s1, AccountID s2, Currency currency) {
+ return currency.equals(balance().currency()) && isFor(s1) && isFor(s2);
+ }
+
+ public Currency currency() {
+ return balance().currency();
+ }
+
+ private Amount issuedBy(boolean hi) {
+ Amount balance;
+
+ if (hi) {
+ balance = balance().newIssuer(highAccount());
+ } else {
+ balance = balance().negate().newIssuer(lowAccount());
+ }
+
+ if (!balance.isPositive()) {
+ balance = balance.issue().amount(0);
+ }
+ return balance;
+ }
+
+ public Amount issuedByHigh() {
+ return issuedBy(true);
+ }
+ public Amount issuedByLow() {
+ return issuedBy(false);
+ }
+
+ public Amount issuedTo(AccountID accountID) {
+ return issuedBy(isLowAccount(accountID));
+ }
+
+ @Deprecated() // "not deprecated but needs fixing"
+ public boolean authorizedBy(AccountID account) {
+ UInt32 flags = flags();
+ return flags == null || flags.testBit(isHighAccount(account) ? LedgerFlag.HighAuth : LedgerFlag.LowAuth);
+ }
+
+ private boolean isBitSet(int flags, int flag) {
+ return (flags & flag) != 0;
+ }
+
+ private boolean isHighAccount(AccountID account) {
+ return highAccount().equals(account);
+ }
+ private boolean isLowAccount(AccountID account) {
+ return lowAccount().equals(account);
+ }
+
+
+ public Hash256 lowNodeOwnerDirectory() {
+ Hash256 ownerDir = Index.ownerDirectory(lowAccount());
+ return Index.directoryNode(ownerDir, lowNode());
+ }
+ public Hash256 highNodeOwnerDirectory() {
+ Hash256 ownerDir = Index.ownerDirectory(highAccount());
+ return Index.directoryNode(ownerDir, highNode());
+ }
+
+ public Hash256[] directoryIndexes() {
+ return new Hash256[]{lowNodeOwnerDirectory(), highNodeOwnerDirectory()};
+ }
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+
+ if (lowNode() == null) {
+ lowNode(UInt64.ZERO);
+ }
+ if (highNode() == null) {
+ highNode(UInt64.ZERO);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/SignerList.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/SignerList.java
new file mode 100644
index 0000000000..68af847605
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/SignerList.java
@@ -0,0 +1,32 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.STArray;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+public class SignerList extends IndexedLedgerEntry {
+ public SignerList() {
+ super(LedgerEntryType.SignerList);
+ }
+
+ @Override
+ public void setDefaults() {
+ super.setDefaults();
+ if (!has(Field.SignerListID)) {
+ put(UInt32.SignerListID, UInt32.ZERO);
+ }
+ }
+
+ public STArray signerEntries() {return get(STArray.SignerEntries);}
+ public UInt32 signerListID() {return get(UInt32.SignerListID);}
+ public UInt32 signerQuorum() {return get(UInt32.SignerQuorum);}
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+
+ public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);}
+ public void signerEntries(STArray val) { put(STArray.SignerEntries, val);}
+ public void signerListID(UInt32 val) { put(UInt32.SignerListID, val);}
+ public void signerQuorum(UInt32 val) { put(UInt32.SignerQuorum, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Ticket.java b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Ticket.java
new file mode 100644
index 0000000000..5d48e3f462
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/sle/entries/Ticket.java
@@ -0,0 +1,27 @@
+package com.ripple.core.types.known.sle.entries;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+
+public class Ticket extends IndexedLedgerEntry {
+ public Ticket() {
+ super(LedgerEntryType.Ticket);
+ }
+
+ public AccountID account() {return get(AccountID.Account);}
+ public AccountID target() {return get(AccountID.Target);}
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public UInt32 sequence() {return get(UInt32.Sequence);}
+ public UInt64 ownerNode() {return get(UInt64.OwnerNode);}
+
+ public void expiration(UInt32 val) { put(UInt32.Expiration, val);}
+ public void ownerNode(UInt64 val) { put(UInt64.OwnerNode, val);}
+ public void sequence(UInt32 val) { put(UInt32.Sequence, val);}
+ public void target(AccountID val) { put(AccountID.Target, val);}
+
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+ public boolean hasTarget() {return has(AccountID.Target);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/Transaction.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/Transaction.java
new file mode 100644
index 0000000000..8f174968aa
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/Transaction.java
@@ -0,0 +1,116 @@
+package com.ripple.core.types.known.tx;
+
+import com.ripple.core.coretypes.*;
+import com.ripple.core.coretypes.hash.HalfSha512;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.uint.UInt16;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.enums.TransactionFlag;
+import com.ripple.core.fields.Field;
+import com.ripple.core.formats.TxFormat;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.signed.SignedTransaction;
+import com.ripple.crypto.keys.IVerifyingKey;
+import com.ripple.utils.HashUtils;
+
+public class Transaction extends STObject {
+ public static final boolean CANONICAL_FLAG_DEPLOYED = true;
+ public static final UInt32 CANONICAL_SIGNATURE = new UInt32(TransactionFlag.FullyCanonicalSig);
+
+ public Transaction(TransactionType type) {
+ setFormat(TxFormat.formats.get(type));
+ put(Field.TransactionType, type);
+ }
+
+ public SignedTransaction prepare(byte[] pubKeyBytes) {
+ SignedTransaction tx = SignedTransaction.fromTx(this);
+ tx.prepare(pubKeyBytes);
+ return tx;
+ }
+
+ public TransactionType transactionType() {
+ return transactionType(this);
+ }
+
+ public Hash256 signingHash() {
+ return signingHash(HashPrefix.txSign);
+ }
+
+ public byte[] signingData() {
+ return signingData(HashPrefix.txSign);
+ }
+
+ public boolean verifySignature(AccountID key) {
+ Blob pubKey = signingPubKey();
+ return IVerifyingKey.from(pubKey.toBytes())
+ .verify(signingData(),
+ txnSignature().toBytes()) &&
+ signingKey().equals(key);
+ }
+
+ public boolean verifyMasterKeySignature() {
+ return verifySignature(account());
+ }
+
+ public void setCanonicalSignatureFlag() {
+ UInt32 flags = get(UInt32.Flags);
+ if (flags == null) {
+ flags = CANONICAL_SIGNATURE;
+ } else {
+ flags = flags.or(CANONICAL_SIGNATURE);
+ }
+ put(UInt32.Flags, flags);
+ }
+
+ public UInt32 flags() {return get(UInt32.Flags);}
+ public UInt32 sourceTag() {return get(UInt32.SourceTag);}
+ public UInt32 sequence() {return get(UInt32.Sequence);}
+ public UInt32 lastLedgerSequence() {return get(UInt32.LastLedgerSequence);}
+ public UInt32 operationLimit() {return get(UInt32.OperationLimit);}
+ public Hash256 previousTxnID() {return get(Hash256.PreviousTxnID);}
+ public Hash256 accountTxnID() {return get(Hash256.AccountTxnID);}
+ public Amount fee() {return get(Amount.Fee);}
+ public Blob signingPubKey() {return get(Blob.SigningPubKey);}
+ public Blob txnSignature() {return get(Blob.TxnSignature);}
+ public AccountID account() {return get(AccountID.Account);}
+
+ public void transactionType(UInt16 val) {put(Field.TransactionType, val);}
+ public void flags(UInt32 val) {put(Field.Flags, val);}
+ public void sourceTag(UInt32 val) {put(Field.SourceTag, val);}
+ public void sequence(UInt32 val) {put(Field.Sequence, val);}
+ public void lastLedgerSequence(UInt32 val) {put(Field.LastLedgerSequence, val);}
+ public void operationLimit(UInt32 val) {put(Field.OperationLimit, val);}
+ public void previousTxnID(Hash256 val) {put(Field.PreviousTxnID, val);}
+ public void accountTxnID(Hash256 val) {put(Field.AccountTxnID, val);}
+ public void fee(Amount val) {put(Field.Fee, val);}
+ public void signingPubKey(Blob val) {put(Field.SigningPubKey, val);}
+ public void txnSignature(Blob val) {put(Field.TxnSignature, val);}
+ public void account(AccountID val) {put(Field.Account, val);}
+
+ public boolean hasFlags() {return has(UInt32.Flags);}
+ public boolean hasSourceTag() {return has(UInt32.SourceTag);}
+ public boolean hasLastLedgerSequence() {return has(UInt32.LastLedgerSequence);}
+ public boolean hasOperationLimit() {return has(UInt32.OperationLimit);}
+ public boolean hasPreviousTxnID() {return has(Hash256.PreviousTxnID);}
+ public boolean hasAccountTxnID() {return has(Hash256.AccountTxnID);}
+ public boolean hasTxnSignature() {return has(Blob.TxnSignature);}
+ public boolean hasSigners() {return has(STArray.Signers);}
+ public boolean hasMemos() {return has(STArray.Memos);}
+
+ public Hash256 hash() {
+ return get(Hash256.hash);
+ }
+
+ public Hash256 createHash() {
+ HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID);
+ toBytesSink(id);
+ return id.finish();
+ }
+
+ public AccountID signingKey() {
+ // May be a regular Key
+ byte[] pubKey = HashUtils.SHA256_RIPEMD160(signingPubKey().toBytes());
+ return AccountID.fromBytes(pubKey);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/AffectedNode.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/AffectedNode.java
new file mode 100644
index 0000000000..9c399b73a0
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/AffectedNode.java
@@ -0,0 +1,156 @@
+package com.ripple.core.types.known.tx.result;
+
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt16;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.SerializedType;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+// TODO: fix up this nonsense
+public class AffectedNode extends STObject {
+ public final Field field;
+ public final STObject nested;
+
+ public AffectedNode(STObject source) {
+ fields = source.getFields();
+ field = getField();
+ nested = nestedObject();
+ }
+
+ public boolean isOffer() {
+ return ledgerEntryType() == LedgerEntryType.Offer;
+ }
+
+ public boolean isAccountRoot() {
+ return ledgerEntryType() == LedgerEntryType.AccountRoot;
+ }
+
+ public boolean isRippleState() {
+ return ledgerEntryType() == LedgerEntryType.RippleState;
+ }
+
+ public boolean isDirectoryNode() {
+ return ledgerEntryType() == LedgerEntryType.DirectoryNode;
+ }
+
+ public boolean wasPreviousNode() {
+ return isDeletedNode() || isModifiedNode();
+ }
+
+ public boolean isFinalNode() {
+ return true;
+ }
+
+ public boolean isCreatedNode() {
+ return field == Field.CreatedNode;
+ }
+
+ public boolean isDeletedNode() {
+ return field == Field.DeletedNode;
+ }
+
+ public boolean isModifiedNode() {
+ return field == Field.ModifiedNode;
+ }
+
+ public Field getField() {
+// return iterator().next();
+ return fields.firstKey();
+ }
+
+ public Hash256 ledgerIndex() {
+ return nested.get(Hash256.LedgerIndex);
+ }
+
+ public LedgerEntryType ledgerEntryType() {
+ return ledgerEntryType(nested);
+ }
+
+ private STObject nestedObject() {
+ return (STObject) get(getField());
+ }
+
+ /**
+ * @return - LedgerEntry before the transaction (or after in the case of
+ * a CreatedNode)
+ */
+ public LedgerEntry nodeAsPrevious() {
+ return (LedgerEntry) rebuildFromMeta(true);
+ }
+
+ public LedgerEntry nodeAsFinal() {
+ return (LedgerEntry) rebuildFromMeta(false);
+ }
+
+ private STObject rebuildFromMeta(boolean asPrevious) {
+ boolean created = isCreatedNode();
+ STObject mixed = new STObject();
+
+ // The first object has only a single key
+ Field wrapperField = created ? Field.CreatedNode :
+ isDeletedNode() ? Field.DeletedNode :
+ Field.ModifiedNode;
+
+ STObject wrapped = (STObject) get(wrapperField);
+
+ Field finalFields = created ? Field.NewFields :
+ Field.FinalFields;
+
+ // You may get some AccountRoot objects like this
+ if (!wrapped.has(finalFields)) {
+ STObject source = new STObject(wrapped.getFields());
+ source.put(Hash256.index, wrapped.get(Hash256.LedgerIndex));
+ source.remove(Field.LedgerIndex);
+ return STObject.formatted(source);
+ }
+
+ // Get all the final fields
+ STObject finals = (STObject) wrapped.get(finalFields);
+ for (Field field : finals) {
+ mixed.put(field, finals.get(field));
+ }
+
+ // Then layer over the previous fields if desired as previous
+ // DirectoryNode LedgerEntryType won't have `PreviousFields`
+ if (asPrevious && wrapped.has(Field.PreviousFields)) {
+ STObject previous = wrapped.get(STObject.PreviousFields);
+ for (Field field : previous) {
+ mixed.put(field, previous.get(field));
+ }
+ }
+
+ // Keep the inner most fields
+ for (Field field : wrapped) {
+ switch (field) {
+ case NewFields:
+ case PreviousFields:
+ case FinalFields:
+ continue;
+ default:
+ SerializedType value = wrapped.get(field);
+ if (field == Field.LedgerIndex) {
+ field = Field.index;
+ }
+ mixed.put(field, value);
+
+ }
+ }
+ return STObject.formatted(mixed);
+ }
+
+ public static boolean isAffectedNode(STObject source) {
+ return (source.size() == 1 && (
+ source.has(DeletedNode) ||
+ source.has(CreatedNode) ||
+ source.has(ModifiedNode)));
+ }
+
+ public boolean removedField(Field field) {
+ return nested.has(Field.PreviousFields) &&
+ nested.get(STObject.PreviousFields).has(field) &&
+ nested.has(Field.FinalFields) &&
+ !nested.get(STObject.FinalFields).has(field);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionMeta.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionMeta.java
new file mode 100644
index 0000000000..c25183436a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionMeta.java
@@ -0,0 +1,50 @@
+package com.ripple.core.types.known.tx.result;
+
+import com.ripple.core.coretypes.STArray;
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt8;
+import com.ripple.core.serialized.enums.EngineResult;
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+import java.util.Iterator;
+
+public class TransactionMeta extends STObject {
+ public static boolean isTransactionMeta(STObject source) {
+ return source.has(UInt8.TransactionResult) &&
+ source.has(STArray.AffectedNodes);
+ }
+
+ public EngineResult engineResult() {
+ return engineResult(this);
+ }
+
+ public Iterable affectedNodes() {
+ STArray nodes = get(STArray.AffectedNodes);
+ final Iterator iterator = nodes.iterator();
+ return () -> iterateAffectedNodes(iterator);
+ }
+
+ private static Iterator iterateAffectedNodes(final Iterator iterator) {
+ return new Iterator() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public AffectedNode next() {
+ return (AffectedNode) iterator.next();
+ }
+
+ @Override
+ public void remove() {
+ iterator.remove();
+ }
+ };
+ }
+
+ public UInt32 transactionIndex() {
+ return get(UInt32.TransactionIndex);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionResult.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionResult.java
new file mode 100644
index 0000000000..2e4467bf14
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/result/TransactionResult.java
@@ -0,0 +1,299 @@
+package com.ripple.core.types.known.tx.result;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.STArray;
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.EngineResult;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.sle.entries.AccountRoot;
+import com.ripple.core.types.known.tx.Transaction;
+import com.ripple.encodings.common.B16;
+import org.json.JSONObject;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+public class TransactionResult implements Comparable {
+ // The json formatting of transaction results is a MESS
+ public enum Source {
+ request_tx_result,
+ request_account_tx,
+ request_account_tx_binary,
+ request_tx_binary,
+ ledger_transactions_expanded_with_ledger_index_injected,
+ transaction_subscription_notification
+ }
+
+ public EngineResult engineResult;
+ public UInt32 ledgerIndex;
+ public Hash256 hash;
+
+ // TODO: in practice this class is GENERALLY only for validated results.
+ // NOT strictly true as you can get results from closed but not validated
+ // ledgers! Though you would want to beyond testing ...
+ public boolean validated;
+
+ public TransactionResult(long ledgerIndex, Hash256 hash, Transaction txn, TransactionMeta meta) {
+ this.ledgerIndex = new UInt32(ledgerIndex);
+ this.hash = hash;
+ this.txn = txn;
+ this.meta = meta;
+ this.engineResult = meta.engineResult();
+ this.validated = true;
+ }
+
+ public Transaction txn;
+ public TransactionMeta meta;
+
+ public TransactionType transactionType() {
+ return txn.transactionType();
+ }
+
+ public AccountID createdAccount() {
+ AccountID destination = null;
+ Hash256 destinationIndex = null;
+
+ if (transactionType() == TransactionType.Payment && meta.has(Field.AffectedNodes)) {
+ STArray affected = meta.get(STArray.AffectedNodes);
+ for (STObject node : affected) {
+ if (node.has(STObject.CreatedNode)) {
+ STObject created = node.get(STObject.CreatedNode);
+ if (STObject.ledgerEntryType(created) == LedgerEntryType.AccountRoot) {
+ if (destination == null) {
+ destination = txn.get(AccountID.Destination);
+ destinationIndex = Index.accountRoot(destination);
+ }
+ if (destinationIndex.equals(created.get(Hash256.LedgerIndex))) {
+ return destination;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public Map modifiedRoots() {
+ TreeMap accounts = null;
+
+ if (meta.has(Field.AffectedNodes)) {
+ accounts = new TreeMap<>();
+ for (AffectedNode fields : meta.affectedNodes()) {
+ if (fields.isModifiedNode() && fields.isAccountRoot()) {
+ AccountRoot root = (AccountRoot) fields.nodeAsFinal();
+ //noinspection StatementWithEmptyBody
+ if (root.account() != null) {
+ accounts.put(root.account(), root);
+ } else {
+ // TODO: Remember why these modified nodes have no
+ // FinalFields or NewFields
+ /*
+ {"ModifiedNode": {
+ "LedgerIndex": "2C6F7594FB7471F4983C2BC691AAC2F25F8DB88D455985B4181E053D7AB23006",
+ "PreviousTxnLgrSeq": 35561097,
+ "LedgerEntryType": "AccountRoot",
+ "index": "2C6F7594FB7471F4983C2BC691AAC2F25F8DB88D455985B4181E053D7AB23006",
+ "PreviousTxnID": "67EE84E892FBEDF5FD52D511FF1E833870A2B8104CA5FF9BA89867A528A5D3ED"
+ }}
+ * */
+ }
+ }
+ }
+ }
+ return accounts;
+ }
+
+ public AccountID initiatingAccount() {
+ return txn.get(AccountID.Account);
+ }
+
+ public int compareTo(TransactionResult o2) {
+ TransactionResult o1 = this;
+ int i = o1.ledgerIndex.compareTo(o2.ledgerIndex);
+ if (i != 0) {
+ return i;
+ } else {
+ return o1.meta.transactionIndex()
+ .compareTo(o2.meta.transactionIndex());
+ }
+ }
+
+ public static TransactionResult fromJSON(ObjectNode json) {
+ // TODO: obviously ...
+ return fromJSON(new JSONObject(json.toString()));
+ }
+
+ public static TransactionResult fromJSON(JSONObject json) {
+ boolean binary;
+
+ String metaKey = json.has("meta") ? "meta" : "metaData";
+
+ String txKey = json.has("transaction") ? "transaction" :
+ json.has("tx") ? "tx" :
+ json.has("tx_blob") ? "tx_blob" : null;
+
+ if (txKey == null && !json.has("TransactionType")) {
+ throw new IllegalArgumentException("This json isn't a transaction " + json);
+ }
+
+ binary = txKey != null && json.get(txKey) instanceof String;
+
+ Transaction txn;
+ if (txKey == null) {
+ // This should parse the `hash` field
+ txn = (Transaction) STObject.fromJSONObject(json);
+ } else {
+ txn = (Transaction) parseObject(json, txKey, binary);
+ if (json.has("hash")) {
+ txn.put(Hash256.hash, Hash256.fromHex(json.getString("hash")));
+ } else if (binary) {
+ byte[] decode = B16.decode(json.getString(txKey));
+ txn.put(Hash256.hash, Index.transactionID(decode));
+ }
+ }
+
+ TransactionMeta meta = (TransactionMeta) parseObject(json, metaKey, binary);
+ long ledger_index = json.optLong("ledger_index", 0);
+ if (ledger_index == 0 && !binary) {
+ ledger_index = json.getJSONObject(txKey).getLong("ledger_index");
+ }
+
+ TransactionResult tr = new TransactionResult(ledger_index, txn.get(Hash256.hash), txn, meta);
+// if (json.has("ledger_hash")) {
+// tr.ledgerHash = Hash256.fromHex(json.getString("ledger_hash"));
+// }
+ return tr;
+ }
+
+ private static STObject parseObject(JSONObject json, String key, boolean binary) {
+ if (binary) {
+ return STObject.fromHex(json.getString(key));
+ } else {
+ JSONObject tx_json = json.getJSONObject(key);
+ return STObject.fromJSONObject(tx_json);
+ }
+ }
+
+ public TransactionResult(JSONObject json, Source resultMessageSource) {
+ if (resultMessageSource == Source.transaction_subscription_notification) {
+
+ engineResult = EngineResult.valueOf(json.getString("engine_result"));
+ validated = json.getBoolean("validated");
+// ledgerHash = Hash256.fromHex(json.getString("ledger_hash"));
+ ledgerIndex = new UInt32(json.getLong("ledger_index"));
+
+ if (json.has("transaction")) {
+ txn = (Transaction) STObject.fromJSONObject(json.getJSONObject("transaction"));
+ hash = txn.get(Hash256.hash);
+ }
+
+ if (json.has("meta")) {
+ meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta"));
+ }
+ }
+ else if (resultMessageSource == Source.ledger_transactions_expanded_with_ledger_index_injected) {
+ validated = true;
+ meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("metaData"));
+ txn = (Transaction) STObject.fromJSONObject(json);
+ hash = txn.get(Hash256.hash);
+ engineResult = meta.engineResult();
+ ledgerIndex = new UInt32(json.getLong("ledger_index"));
+
+ } else if (resultMessageSource == Source.request_tx_result) {
+ validated = json.optBoolean("validated", false);
+ if (validated && !json.has("meta")) {
+ throw new IllegalStateException("It's validated, why doesn't it have meta??");
+ }
+ if (validated) {
+ meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta"));
+ engineResult = meta.engineResult();
+ txn = (Transaction) STObject.fromJSONObject(json);
+ hash = txn.get(Hash256.hash);
+ ledgerIndex = new UInt32(json.getLong("ledger_index"));
+
+ }
+ } else if (resultMessageSource == Source.request_account_tx) {
+ validated = json.optBoolean("validated", false);
+ if (validated && !json.has("meta")) {
+ throw new IllegalStateException("It's validated, why doesn't it have meta??");
+ }
+ if (validated) {
+ JSONObject tx = json.getJSONObject("tx");
+ meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta"));
+ engineResult = meta.engineResult();
+ this.txn = (Transaction) STObject.fromJSONObject(tx);
+ hash = this.txn.get(Hash256.hash);
+ ledgerIndex = new UInt32(tx.getLong("ledger_index"));
+ }
+ } else if (resultMessageSource == Source.request_account_tx_binary || resultMessageSource == Source.request_tx_binary) {
+ validated = json.optBoolean("validated", false);
+ if (validated && !json.has("meta")) {
+ throw new IllegalStateException("It's validated, why doesn't it have meta??");
+ }
+ if (validated) {
+ /*
+ {
+ "ledger_index": 3378767,
+ "meta": "201 ...",
+ "tx_blob": "120 ...",
+ "validated": true
+ },
+ */
+ boolean account_tx = resultMessageSource == Source.request_account_tx_binary;
+
+ String tx = json.getString(account_tx ? "tx_blob" : "tx");
+ byte[] decodedTx = B16.decode(tx);
+ meta = (TransactionMeta) STObject.fromHex(json.getString("meta"));
+ this.txn = (Transaction) STObject.fromBytes(decodedTx);
+
+ if (account_tx) {
+ hash = Index.transactionID(decodedTx);
+ } else {
+ hash = Hash256.fromHex(json.getString("hash"));
+ }
+ this.txn.put(Field.hash, hash);
+
+ engineResult = meta.engineResult();
+ ledgerIndex = new UInt32(json.getLong("ledger_index"));
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ JSONObject object = toJSON();
+ return object.toString();
+ }
+
+ public JSONObject toJSON() {
+ JSONObject o = new JSONObject();
+ o.put("tx", txn.toJSON());
+ o.put("meta", meta.toJSON());
+ o.put("ledger_index", ledgerIndex);
+ o.put("hash", hash.toHex());
+ return o;
+ }
+
+ public TransactionResult copy() {
+ TransactionMeta metaCopy = (TransactionMeta) STObject.fromBytes(meta.toBytes());
+ Transaction txnCopy = (Transaction) STObject.fromBytes(txn.toBytes());
+ return new TransactionResult(ledgerIndex.longValue(), hash, txnCopy, metaCopy);
+ }
+
+ public JSONObject toJSONBinary() {
+ JSONObject o = new JSONObject();
+
+ o.put("hash", hash.toHex());
+ o.put("meta", meta.toHex());
+ o.put("tx", txn.toHex());
+ o.put("ledger_index", ledgerIndex);
+
+ return o;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/signed/SignedTransaction.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/signed/SignedTransaction.java
new file mode 100644
index 0000000000..56440a9bf1
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/signed/SignedTransaction.java
@@ -0,0 +1,94 @@
+package com.ripple.core.types.known.tx.signed;
+
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.hash.HalfSha512;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.BytesList;
+import com.ripple.core.serialized.MultiSink;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+import java.util.Arrays;
+
+public class SignedTransaction {
+ private SignedTransaction(Transaction of) {
+ // TODO: is this just over kill ?
+ txn = (Transaction) STObject.fromBytes(of.toBytes());
+ }
+
+ protected SignedTransaction() {
+ }
+
+ public Transaction txn;
+ public Hash256 hash;
+
+ public byte[] signingData;
+ public byte[] previousSigningData;
+ public String tx_blob;
+
+ public static SignedTransaction fromTx(Transaction tx) {
+ return new SignedTransaction(tx);
+ }
+
+ public void prepare(byte[] pubKeyBytes) {
+ prepare(pubKeyBytes,null, null, null);
+ }
+
+ public void prepare(byte[] pubKeyBytes,
+ Amount fee,
+ UInt32 Sequence,
+ UInt32 lastLedgerSequence) {
+
+ Blob pubKey = new Blob(pubKeyBytes);
+
+ // This won't always be specified
+ if (lastLedgerSequence != null) {
+ txn.put(UInt32.LastLedgerSequence, lastLedgerSequence);
+ }
+ if (Sequence != null) {
+ txn.put(UInt32.Sequence, Sequence);
+ }
+ if (fee != null) {
+ txn.put(Amount.Fee, fee);
+ }
+
+ txn.signingPubKey(pubKey);
+
+ if (Transaction.CANONICAL_FLAG_DEPLOYED) {
+ txn.setCanonicalSignatureFlag();
+ }
+
+ txn.checkFormat();
+ signingData = txn.signingData();
+ if (previousSigningData != null && Arrays.equals(signingData, previousSigningData)) {
+ return;
+ }
+ }
+
+ public void addSign(byte[] signature) {
+ try {
+ txn.txnSignature(new Blob(signature));
+
+ BytesList blob = new BytesList();
+ HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID);
+
+ txn.toBytesSink(new MultiSink(blob, id));
+ tx_blob = blob.bytesHex();
+ hash = id.finish();
+ } catch (Exception e) {
+ // electric paranoia
+ previousSigningData = null;
+ throw new RuntimeException(e);
+ } /*else {*/
+ previousSigningData = signingData;
+ // }
+ }
+
+ public TransactionType transactionType() {
+ return txn.transactionType();
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/AccountSet.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/AccountSet.java
new file mode 100644
index 0000000000..0e8a1c9672
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/AccountSet.java
@@ -0,0 +1,46 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.hash.Hash128;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt8;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class AccountSet extends Transaction{
+ public AccountSet() {
+ super(TransactionType.AccountSet);
+ }
+ public UInt32 transferRate() {return get(UInt32.TransferRate);}
+ public UInt32 walletSize() {return get(UInt32.WalletSize);}
+ public UInt32 setFlag() {return get(UInt32.SetFlag);}
+ public UInt32 clearFlag() {return get(UInt32.ClearFlag);}
+ public Hash128 emailHash() {return get(Hash128.EmailHash);}
+ public Hash256 walletLocator() {return get(Hash256.WalletLocator);}
+ public Blob messageKey() {return get(Blob.MessageKey);}
+ public Blob domain() {return get(Blob.Domain);}
+ public UInt8 tickSize() {return get(UInt8.TickSize);}
+
+ public void transferRate(UInt32 val) {put(Field.TransferRate, val);}
+ public void walletSize(UInt32 val) {put(Field.WalletSize, val);}
+ public void setFlag(UInt32 val) {put(Field.SetFlag, val);}
+ public void clearFlag(UInt32 val) {put(Field.ClearFlag, val);}
+ public void emailHash(Hash128 val) {put(Field.EmailHash, val);}
+ public void walletLocator(Hash256 val) {put(Field.WalletLocator, val);}
+ public void messageKey(Blob val) {put(Field.MessageKey, val);}
+ public void domain(Blob val) {put(Field.Domain, val);}
+ public void tickSize(UInt8 val) { put(UInt8.TickSize, val);}
+
+ public boolean hasTransferRate() {return has(UInt32.TransferRate);}
+ public boolean hasWalletSize() {return has(UInt32.WalletSize);}
+ public boolean hasSetFlag() {return has(UInt32.SetFlag);}
+ public boolean hasClearFlag() {return has(UInt32.ClearFlag);}
+ public boolean hasEmailHash() {return has(Hash128.EmailHash);}
+ public boolean hasWalletLocator() {return has(Hash256.WalletLocator);}
+ public boolean hasMessageKey() {return has(Blob.MessageKey);}
+ public boolean hasDomain() {return has(Blob.Domain);}
+ public boolean hasTickSize() {return has(UInt8.TickSize);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCancel.java
new file mode 100644
index 0000000000..f5b961f514
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCancel.java
@@ -0,0 +1,14 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class CheckCancel extends Transaction {
+ public CheckCancel() {
+ super(TransactionType.CheckCancel);
+ }
+
+ public Hash256 checkID() {return get(Hash256.CheckID);}
+ public void checkID(Hash256 val) { put(Hash256.CheckID, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCash.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCash.java
new file mode 100644
index 0000000000..8eba2458e9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCash.java
@@ -0,0 +1,23 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class CheckCash extends Transaction {
+ public CheckCash() {
+ super(TransactionType.CheckCash);
+ }
+
+ public boolean hasAmount() {return has(Amount.Amount);}
+ public boolean hasDeliverMin() {return has(Amount.DeliverMin);}
+
+ public Hash256 checkID() {return get(Hash256.CheckID);}
+ public Amount amount() {return get(Amount.Amount);}
+ public Amount deliverMin() {return get(Amount.DeliverMin);}
+
+ public void checkID(Hash256 val) { put(Hash256.CheckID, val);}
+ public void amount(Amount val) { put(Amount.Amount, val);}
+ public void deliverMin(Amount val) { put(Amount.DeliverMin, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCreate.java
new file mode 100644
index 0000000000..b8e75a9b0f
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/CheckCreate.java
@@ -0,0 +1,30 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class CheckCreate extends Transaction {
+ public CheckCreate() {
+ super(TransactionType.CheckCreate);
+ }
+
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasInvoiceID() {return has(Hash256.InvoiceID);}
+
+ public void expiration(UInt32 val) { put(UInt32.Expiration, val);}
+ public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);}
+ public void invoiceID(Hash256 val) { put(Hash256.InvoiceID, val);}
+ public void sendMax(Amount val) { put(Amount.SendMax, val);}
+ public void destination(AccountID val) { put(AccountID.Destination, val); }
+
+ public AccountID destination() {return get(AccountID.Destination);}
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public Hash256 invoiceID() {return get(Hash256.InvoiceID);}
+ public Amount sendMax() {return get(Amount.SendMax);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/DepositPreauth.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/DepositPreauth.java
new file mode 100644
index 0000000000..68f08cde32
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/DepositPreauth.java
@@ -0,0 +1,19 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class DepositPreauth extends Transaction {
+ public DepositPreauth() {
+ super(TransactionType.DepositPreauth);
+ }
+ public boolean hasAuthorize() {return has(AccountID.Authorize);}
+ public AccountID authorize() {return get(AccountID.Authorize);}
+ public void authorize(AccountID val) { put(AccountID.Authorize, val);}
+
+ public boolean hasUnauthorize() {return has(AccountID.Unauthorize);}
+ public AccountID unauthorize() {return get(AccountID.Unauthorize);}
+ public void unauthorize(AccountID val) { put(AccountID.Unauthorize, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCancel.java
new file mode 100644
index 0000000000..ab435bacda
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCancel.java
@@ -0,0 +1,18 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class EscrowCancel extends Transaction {
+ public EscrowCancel() {
+ super(TransactionType.EscrowCancel);
+ }
+
+ public UInt32 offerSequence() {return get(UInt32.OfferSequence);}
+ public void offerSequence(UInt32 val) { put(UInt32.OfferSequence, val);}
+ public AccountID owner() {return get(AccountID.Owner);}
+ public void owner(AccountID val) { put(AccountID.Owner, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCreate.java
new file mode 100644
index 0000000000..3587a6c191
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowCreate.java
@@ -0,0 +1,33 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class EscrowCreate extends Transaction {
+ public EscrowCreate() {
+ super(TransactionType.EscrowCreate);
+ }
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasCancelAfter() {return has(UInt32.CancelAfter);}
+ public boolean hasFinishAfter() {return has(UInt32.FinishAfter);}
+ public boolean hasCondition() {return has(Blob.Condition);}
+
+ public Amount amount() {return get(Amount.Amount);}
+ public AccountID destination() {return get(AccountID.Destination);}
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public UInt32 cancelAfter() {return get(UInt32.CancelAfter);}
+ public UInt32 finishAfter() {return get(UInt32.FinishAfter);}
+ public Blob condition() {return get(Blob.Condition);}
+
+ public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);}
+ public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);}
+ public void finishAfter(UInt32 val) { put(UInt32.FinishAfter, val);}
+ public void amount(Amount val) { put(Amount.Amount, val);}
+ public void condition(Blob val) { put(Blob.Condition, val);}
+ public void destination(AccountID val) { put(AccountID.Destination, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowFinish.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowFinish.java
new file mode 100644
index 0000000000..7b552e768d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/EscrowFinish.java
@@ -0,0 +1,27 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class EscrowFinish extends Transaction {
+ public EscrowFinish() {
+ super(TransactionType.EscrowFinish);
+ }
+
+ public boolean hasFulfillment() {return has(Blob.Fulfillment);}
+ public boolean hasCondition() {return has(Blob.Condition);}
+
+ public UInt32 offerSequence() {return get(UInt32.OfferSequence);}
+ public Blob fulfillment() {return get(Blob.Fulfillment);}
+ public Blob condition() {return get(Blob.Condition);}
+ public AccountID owner() {return get(AccountID.Owner);}
+
+ public void offerSequence(UInt32 val) { put(UInt32.OfferSequence, val);}
+ public void fulfillment(Blob val) { put(Blob.Fulfillment, val);}
+ public void condition(Blob val) { put(Blob.Condition, val);}
+ public void owner(AccountID val) { put(AccountID.Owner, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCancel.java
new file mode 100644
index 0000000000..461d6cac09
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCancel.java
@@ -0,0 +1,13 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class OfferCancel extends Transaction {
+ public OfferCancel() {
+ super(TransactionType.OfferCancel);
+ }
+ public UInt32 offerSequence() {return get(UInt32.OfferSequence);}
+ public void offerSequence(UInt32 val) { put(UInt32.OfferSequence, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCreate.java
new file mode 100644
index 0000000000..dbb783479d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/OfferCreate.java
@@ -0,0 +1,25 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class OfferCreate extends Transaction {
+ public OfferCreate() {
+ super(TransactionType.OfferCreate);
+ }
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public UInt32 offerSequence() {return get(UInt32.OfferSequence);}
+ public Amount takerPays() {return get(Amount.TakerPays);}
+ public Amount takerGets() {return get(Amount.TakerGets);}
+ public void expiration(UInt32 val) {put(Field.Expiration, val);}
+ public void offerSequence(UInt32 val) {put(Field.OfferSequence, val);}
+ public void takerPays(Amount val) {put(Field.TakerPays, val);}
+ public void takerGets(Amount val) {put(Field.TakerGets, val);}
+
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+ public boolean hasOfferSequence() {return has(UInt32.OfferSequence);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/Payment.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/Payment.java
new file mode 100644
index 0000000000..10da726d71
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/Payment.java
@@ -0,0 +1,39 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.PathSet;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class Payment extends Transaction {
+ public Payment() {
+ super(TransactionType.Payment);
+ }
+
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public Hash256 invoiceID() {return get(Hash256.InvoiceID);}
+ public Amount amount() {return get(Amount.Amount);}
+ public Amount sendMax() {return get(Amount.SendMax);}
+ public AccountID destination() {return get(AccountID.Destination);}
+
+ public PathSet paths() {return get(PathSet.Paths);}
+ public Amount deliverMin() {return get(Amount.DeliverMin);}
+
+ public void destinationTag(UInt32 val) {put(Field.DestinationTag, val);}
+ public void invoiceID(Hash256 val) {put(Field.InvoiceID, val);}
+ public void deliverMin(Amount val) { put(Amount.DeliverMin, val);}
+ public void amount(Amount val) {put(Field.Amount, val);}
+ public void sendMax(Amount val) {put(Field.SendMax, val);}
+ public void destination(AccountID val) {put(Field.Destination, val);}
+ public void paths(PathSet val) {put(Field.Paths, val);}
+
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasInvoiceID() {return has(Hash256.InvoiceID);}
+ public boolean hasSendMax() {return has(Amount.SendMax);}
+ public boolean hasDeliverMin() {return has(Amount.DeliverMin);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelClaim.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelClaim.java
new file mode 100644
index 0000000000..8632dbd8b3
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelClaim.java
@@ -0,0 +1,31 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class PaymentChannelClaim extends Transaction {
+ public PaymentChannelClaim() {
+ super(TransactionType.PaymentChannelClaim);
+ }
+
+ public boolean hasAmount() {return has(Amount.Amount);}
+ public boolean hasBalance() {return has(Amount.Balance);}
+ public boolean hasPublicKey() {return has(Blob.PublicKey);}
+ public boolean hasSignature() {return has(Blob.Signature);}
+
+ public void channel(Hash256 val) { put(Hash256.Channel, val);}
+ public void amount(Amount val) { put(Amount.Amount, val);}
+ public void balance(Amount val) { put(Amount.Balance, val);}
+ public void publicKey(Blob val) { put(Blob.PublicKey, val);}
+ public void signature(Blob val) { put(Blob.Signature, val);}
+
+ public Hash256 channel() {return get(Hash256.Channel);}
+ public Amount amount() {return get(Amount.Amount);}
+ public Amount balance() {return get(Amount.Balance);}
+ public Blob publicKey() {return get(Blob.PublicKey);}
+ public Blob signature() {return get(Blob.Signature);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelCreate.java
new file mode 100644
index 0000000000..2475c637e5
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelCreate.java
@@ -0,0 +1,32 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.Blob;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class PaymentChannelCreate extends Transaction {
+ public PaymentChannelCreate() {
+ super(TransactionType.PaymentChannelCreate);
+ }
+
+ public boolean hasDestinationTag() {return has(UInt32.DestinationTag);}
+ public boolean hasCancelAfter() {return has(UInt32.CancelAfter);}
+
+ public void destinationTag(UInt32 val) { put(UInt32.DestinationTag, val);}
+ public void cancelAfter(UInt32 val) { put(UInt32.CancelAfter, val);}
+ public void settleDelay(UInt32 val) { put(UInt32.SettleDelay, val);}
+ public void amount(Amount val) { put(Amount.Amount, val);}
+ public void publicKey(Blob val) { put(Blob.PublicKey, val);}
+ public void destination(AccountID val) { put(AccountID.Destination, val);}
+
+ public UInt32 destinationTag() {return get(UInt32.DestinationTag);}
+ public UInt32 cancelAfter() {return get(UInt32.CancelAfter);}
+ public UInt32 settleDelay() {return get(UInt32.SettleDelay);}
+ public Amount amount() {return get(Amount.Amount);}
+ public Blob publicKey() {return get(Blob.PublicKey);}
+ public AccountID destination() {return get(AccountID.Destination);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelFund.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelFund.java
new file mode 100644
index 0000000000..85ccff90f4
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/PaymentChannelFund.java
@@ -0,0 +1,24 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class PaymentChannelFund extends Transaction {
+ public PaymentChannelFund() {
+ super(TransactionType.PaymentChannelFund);
+ }
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public Hash256 channel() {return get(Hash256.Channel);}
+ public Amount amount() {return get(Amount.Amount);}
+
+ public void expiration(UInt32 val) { put(UInt32.Expiration, val);}
+ public void channel(Hash256 val) { put(Hash256.Channel, val);}
+ public void amount(Amount val) { put(Amount.Amount, val);}
+
+}
+
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SetRegularKey.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SetRegularKey.java
new file mode 100644
index 0000000000..5d139c75f9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SetRegularKey.java
@@ -0,0 +1,14 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class SetRegularKey extends Transaction {
+ public SetRegularKey() {
+ super(TransactionType.SetRegularKey);
+ }
+ public boolean hasRegularKey() {return has(AccountID.RegularKey);}
+ public AccountID regularKey() {return get(AccountID.RegularKey);}
+ public void regularKey(AccountID val) { put(AccountID.RegularKey, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SignerListSet.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SignerListSet.java
new file mode 100644
index 0000000000..830381f8ff
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/SignerListSet.java
@@ -0,0 +1,20 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.STArray;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class SignerListSet extends Transaction {
+ public SignerListSet() {
+ super(TransactionType.SignerListSet);
+ }
+ public boolean hasSignerEntries() {return has(STArray.SignerEntries);}
+
+ public UInt32 signerQuorum() {return get(UInt32.SignerQuorum);}
+ public STArray signerEntries() {return get(STArray.SignerEntries);}
+
+ public void signerQuorum(UInt32 val) { put(UInt32.SignerQuorum, val);}
+ public void signerEntries(STArray val) { put(STArray.SignerEntries, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCancel.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCancel.java
new file mode 100644
index 0000000000..a75f525e52
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCancel.java
@@ -0,0 +1,17 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class TicketCancel extends Transaction {
+ public TicketCancel() {
+ super(TransactionType.TicketCancel);
+ }
+ public Hash256 ticketID() {
+ return get(Hash256.TicketID);
+ }
+ public void ticketID(Hash256 id) {
+ put(Hash256.TicketID, id);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCreate.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCreate.java
new file mode 100644
index 0000000000..4e8ac5fb3f
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TicketCreate.java
@@ -0,0 +1,22 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.AccountID;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class TicketCreate extends Transaction {
+ public TicketCreate() {
+ super(TransactionType.TicketCreate);
+ }
+
+ public boolean hasExpiration() {return has(UInt32.Expiration);}
+ public boolean hasTarget() {return has(AccountID.Target);}
+
+ public UInt32 expiration() {return get(UInt32.Expiration);}
+ public AccountID target() {return get(AccountID.Target);}
+
+ public void expiration(UInt32 val) { put(UInt32.Expiration, val);}
+ public void target(AccountID val) { put(AccountID.Target, val);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TrustSet.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TrustSet.java
new file mode 100644
index 0000000000..ba5702c2e4
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/TrustSet.java
@@ -0,0 +1,25 @@
+package com.ripple.core.types.known.tx.txns;
+
+import com.ripple.core.coretypes.Amount;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class TrustSet extends Transaction {
+ public TrustSet() {
+ super(TransactionType.TrustSet);
+ }
+
+ public UInt32 qualityIn() {return get(UInt32.QualityIn);}
+ public UInt32 qualityOut() {return get(UInt32.QualityOut);}
+ public Amount limitAmount() {return get(Amount.LimitAmount);}
+ public void qualityIn(UInt32 val) {put(Field.QualityIn, val);}
+ public void qualityOut(UInt32 val) {put(Field.QualityOut, val);}
+ public void limitAmount(Amount val) {put(Field.LimitAmount, val);}
+
+ public boolean hasQualityIn() {return has(UInt32.QualityIn);}
+ public boolean hasQualityOut() {return has(UInt32.QualityOut);}
+ public boolean hasLimitAmount() {return has(Amount.LimitAmount);}
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/EnableAmendment.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/EnableAmendment.java
new file mode 100644
index 0000000000..b65fc929e6
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/EnableAmendment.java
@@ -0,0 +1,17 @@
+package com.ripple.core.types.known.tx.txns.pseudo;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class EnableAmendment extends Transaction {
+ public EnableAmendment() {
+ super(TransactionType.EnableAmendment);
+ }
+
+ public UInt32 ledgerSequence() {return get(UInt32.LedgerSequence);}
+ public void ledgerSequence(UInt32 val) { put(UInt32.LedgerSequence, val);}
+ public Hash256 amendment() {return get(Hash256.Amendment);}
+ public void amendment(Hash256 val) { put(Hash256.Amendment, val);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/SetFee.java b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/SetFee.java
new file mode 100644
index 0000000000..800f13e35f
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/known/tx/txns/pseudo/SetFee.java
@@ -0,0 +1,25 @@
+package com.ripple.core.types.known.tx.txns.pseudo;
+
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.serialized.enums.TransactionType;
+import com.ripple.core.types.known.tx.Transaction;
+
+public class SetFee extends Transaction {
+ public SetFee() {
+ super(TransactionType.SetFee);
+ }
+ public boolean hasLedgerSequence() {return has(UInt32.LedgerSequence);}
+
+ public void ledgerSequence(UInt32 val) { put(UInt32.LedgerSequence, val);}
+ public void referenceFeeUnits(UInt32 val) { put(UInt32.ReferenceFeeUnits, val);}
+ public void reserveBase(UInt32 val) { put(UInt32.ReserveBase, val);}
+ public void reserveIncrement(UInt32 val) { put(UInt32.ReserveIncrement, val);}
+ public void baseFee(UInt64 val) { put(UInt64.BaseFee, val);}
+
+ public UInt32 ledgerSequence() {return get(UInt32.LedgerSequence);}
+ public UInt32 referenceFeeUnits() {return get(UInt32.ReferenceFeeUnits);}
+ public UInt32 reserveBase() {return get(UInt32.ReserveBase);}
+ public UInt32 reserveIncrement() {return get(UInt32.ReserveIncrement);}
+ public UInt64 baseFee() {return get(UInt64.BaseFee);}
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/ledger/LedgerHeader.java b/ripple-core/src/main/java/com/ripple/core/types/ledger/LedgerHeader.java
new file mode 100644
index 0000000000..240b0f6da8
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/ledger/LedgerHeader.java
@@ -0,0 +1,109 @@
+package com.ripple.core.types.ledger;
+
+import com.ripple.core.binary.STReader;
+import com.ripple.core.coretypes.RippleDate;
+import com.ripple.core.coretypes.hash.HalfSha512;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.coretypes.uint.UInt64;
+import com.ripple.core.coretypes.uint.UInt8;
+import com.ripple.core.serialized.BinaryParser;
+import com.ripple.core.serialized.BytesSink;
+import org.json.JSONObject;
+import org.json.JSONWriter;
+
+import java.util.Date;
+
+public class LedgerHeader {
+ // Always 0x4C475200 (LWR) (Secures signed objects)
+ public UInt32 version = HashPrefix.ledgerMaster.uInt32();
+
+ public UInt32 sequence; // Ledger Sequence (0 for genesis ledger)
+ public UInt64 totalXRP; //
+ public Hash256 previousLedger; // The hash of the previous ledger (0 for genesis ledger)
+ public Hash256 transactionHash; // The hash of the transaction tree's root node.
+ public Hash256 stateHash; // The hash of the state tree's root node.
+ public UInt32 parentCloseTime; // The time the previous ledger closed
+ public UInt32 closeTime; // UTC minute ledger closed encoded as seconds since 1/1/2000 (or 0 for genesis ledger)
+ public UInt8 closeResolution; // The resolution (in seconds) of the close time
+ public UInt8 closeFlags; // Flags
+
+ public Date closeDate;
+
+ public static LedgerHeader fromParser(BinaryParser parser) {
+ return fromReader(new STReader(parser));
+ }
+ public static LedgerHeader fromHex(String ledger_data) {
+ return LedgerHeader.fromParser(new BinaryParser(ledger_data));
+ }
+
+ public static LedgerHeader fromReader(STReader reader) {
+ LedgerHeader ledger = new LedgerHeader();
+
+ ledger.sequence = reader.uInt32();
+ ledger.totalXRP = reader.uInt64();
+ ledger.previousLedger = reader.hash256();
+ ledger.transactionHash= reader.hash256();
+ ledger.stateHash = reader.hash256();
+ ledger.parentCloseTime = reader.uInt32();
+ ledger.closeTime = reader.uInt32();
+ ledger.closeResolution = reader.uInt8();
+ ledger.closeFlags = reader.uInt8();
+
+ ledger.closeDate = RippleDate.fromSecondsSinceRippleEpoch(ledger.closeTime);
+
+ return ledger;
+ }
+
+ public void toBytesSink(BytesSink sink) {
+ sequence.toBytesSink(sink);
+ totalXRP.toBytesSink(sink);
+ previousLedger.toBytesSink(sink);
+ transactionHash.toBytesSink(sink);
+ stateHash.toBytesSink(sink);
+ parentCloseTime.toBytesSink(sink);
+ closeTime.toBytesSink(sink);
+ closeResolution.toBytesSink(sink);
+ closeFlags.toBytesSink(sink);
+ }
+
+ public Hash256 hash() {
+ HalfSha512 half = HalfSha512.prefixed256(HashPrefix.ledgerMaster);
+ toBytesSink(half);
+ return half.finish();
+ }
+
+ public void toJSONWriter(JSONWriter writer) {
+ writer.key("ledger_index");
+ writer.value(sequence.toJSON());
+ writer.key("total_coins");
+ writer.value(totalXRP.toString(10));
+ writer.key("parent_hash");
+ writer.value(previousLedger.toJSON());
+ writer.key("transaction_hash");
+ writer.value(transactionHash.toJSON());
+ writer.key("account_hash");
+ writer.value(stateHash.toJSON());
+ writer.key("close_time");
+ writer.value(closeTime.toJSON());
+ writer.key("close_time_human");
+ // TODO
+ writer.value(RippleDate.gmtString(RippleDate.fromSecondsSinceRippleEpoch(closeTime)));
+ writer.key("parent_close_time");
+ writer.value(parentCloseTime.toJSON());
+ writer.key("close_time_resolution");
+ writer.value(closeResolution.toJSON());
+ writer.key("close_flags");
+ writer.value(closeFlags.toJSON());
+ }
+
+ public JSONObject toJSON() {
+ StringBuilder builder = new StringBuilder();
+ JSONWriter jsonWriter = new JSONWriter(builder);
+ jsonWriter.object();
+ toJSONWriter(jsonWriter);
+ jsonWriter.endObject();
+ return new JSONObject(builder.toString());
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountState.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountState.java
new file mode 100644
index 0000000000..c6eaadd3fa
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountState.java
@@ -0,0 +1,341 @@
+package com.ripple.core.types.shamap;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.Vector256;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.Index;
+import com.ripple.core.coretypes.uint.UInt32;
+import com.ripple.core.serialized.enums.LedgerEntryType;
+import com.ripple.core.types.known.sle.LedgerEntry;
+import com.ripple.core.types.known.sle.LedgerHashes;
+import com.ripple.core.types.known.sle.entries.DirectoryNode;
+import com.ripple.core.types.known.sle.entries.OfferDirectory;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.json.JSONWriter;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Iterator;
+
+public class AccountState extends ShaMap {
+ public AccountState() {
+ super();
+ }
+ public AccountState(boolean isCopy, int depth) {
+ super(isCopy, depth);
+ }
+
+ @Override
+ protected ShaMapInner makeInnerOfSameClass(int depth) {
+ return new AccountState(true, depth);
+ }
+
+ private static LedgerHashes newSkipList(Hash256 skipIndex) {
+ LedgerHashes skip;
+ skip = new LedgerHashes();
+ skip.put(UInt32.Flags, new UInt32(0));
+ skip.hashes(new Vector256());
+ skip.index(skipIndex);
+ return skip;
+ }
+
+ public void updateSkipLists(long currentIndex, Hash256 parentHash) {
+ long prev = currentIndex - 1;
+
+ if ((prev & 0xFF) == 0) {
+ Hash256 skipIndex = Index.ledgerHashes(prev);
+ LedgerHashes skip = createOrUpdateSkipList(skipIndex);
+ Vector256 hashes = skip.hashes();
+ assert hashes.size() <= 256;
+ hashes.add(parentHash);
+ skip.put(UInt32.LastLedgerSequence, new UInt32(prev));
+ }
+
+ Hash256 skipIndex = Index.ledgerHashes();
+ LedgerHashes skip = createOrUpdateSkipList(skipIndex);
+ Vector256 hashes = skip.hashes();
+
+ if (hashes.size() > 256) throw new AssertionError();
+ if (hashes.size() == 256) {
+ hashes.remove(0);
+ }
+
+ hashes.add(parentHash);
+ skip.put(UInt32.LastLedgerSequence, new UInt32(prev));
+ }
+
+ private LedgerHashes createOrUpdateSkipList(Hash256 skipIndex) {
+ PathToIndex path = pathToIndex(skipIndex);
+ ShaMapInner top = path.dirtyOrCopyInners();
+ LedgerEntryItem item;
+
+ if (path.hasMatchedLeaf()) {
+ ShaMapLeaf leaf = path.invalidatedPossiblyCopiedLeafForUpdating();
+ item = (LedgerEntryItem) leaf.item;
+ } else {
+ item = new LedgerEntryItem(newSkipList(skipIndex));
+ top.addLeafToTerminalInner(new ShaMapLeaf(skipIndex, item));
+ }
+ return (LedgerHashes) item.entry;
+ }
+
+ public boolean addLE(LedgerEntry entry) {
+ LedgerEntryItem item = new LedgerEntryItem(entry);
+ return addItem(entry.index(), item);
+ }
+
+ public boolean updateLE(LedgerEntry entry) {
+ LedgerEntryItem item = new LedgerEntryItem(entry);
+ return updateItem(entry.index(), item);
+ }
+
+ public LedgerEntry getLE(Hash256 index) {
+ LedgerEntryItem item = (LedgerEntryItem) getItem(index);
+ return item == null ? null : item.value();
+ }
+
+ public DirectoryNode getDirectoryNode(Hash256 index) {
+ return (DirectoryNode) getLE(index);
+ }
+
+ public Iterable offerDirectories(Hash256 bookBase) {
+ final QualityIterator iter = qualityIterator(bookBase);
+
+ return new Iterable() {
+ @Override
+ public Iterator iterator() {
+ return new Iterator() {
+ @Override
+ public boolean hasNext() {
+ boolean hasNext = iter.hasNext();
+ // In case we need to skip some entries
+ if (hasNext && !(iter.next() instanceof OfferDirectory)) {
+ return this.hasNext();
+ }
+ return hasNext;
+ }
+
+ @Override
+ public OfferDirectory next() {
+ return (OfferDirectory) iter.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ };
+ }
+
+ public void writeEntriesArray(final JSONWriter writer) {
+ writer.array();
+ walkEntries(entry -> writer.value(entry.toJSON()));
+ writer.endArray();
+ }
+
+ // Assumes shamap won't be modified during iteration, not unusual for an
+ // iterator.
+ public class QualityIterator implements Iterator {
+ ShaMapInner[] inners = new ShaMapInner[64];
+ Hash256 base;
+ Hash256 end;
+ int[] selections = new int[64];
+ int commonNibblets;
+ int depth;
+ ShaMapLeaf next;
+ private boolean finished = false;
+
+ public QualityIterator(Hash256 start) {
+ base = start;
+ depth=0;
+ end = Index.bookEnd(start);
+ ShaMapInner inner = AccountState.this;
+ setInner(inner);
+ setSelected(start.nibblet(depth));
+ findCommonNibblets();
+ }
+
+ private void findCommonNibblets() {
+ for (int i = 0; i < 64; i++) {
+ if (base.nibblet(i) == end.nibblet(i)) {
+ commonNibblets = i;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private void setInner(ShaMapInner inner) {
+ inners[depth] = inner;
+ }
+
+ private void findNext() {
+ next = null;
+
+ while (true) {
+ while (selected() > 15) {
+ depth--;
+ incrementSelection();
+ }
+ if (depth < commonNibblets && selected() > end.nibblet(depth)) {
+ finished = true;
+ break;
+ }
+ ShaMapInner current = currentInner();
+ ShaMapNode branch = current.getBranch(selected());
+ if (branch == null) {
+ incrementSelection();
+ } else if (branch.isInner()) {
+ depth++;
+ setInner(branch.asInner());
+ setSelected(base.nibblet(depth));
+ } else if (branch.isLeaf()) {
+ ShaMapLeaf leaf = branch.asLeaf();
+ Hash256 leafIndex = leaf.index;
+ boolean leafIsOnPathToBase = depth < commonNibblets;
+ if ( leafIsOnPathToBase || leafIndex.compareTo(base) > 0 &&
+ leafIndex.compareTo(end) < 0) {
+ next = leaf;
+ incrementSelection();
+ } else {
+ finished = true;
+ }
+ break;
+ }
+ }
+ }
+
+ private ShaMapInner currentInner() {
+ return inners[depth];
+ }
+ private int selected() {
+ return selections[depth];
+ }
+ private void setSelected(int nibblet) {
+ selections[depth] = nibblet;
+ }
+ private void incrementSelection() {
+ selections[depth]++;
+ }
+
+ @Override
+ public boolean hasNext() {
+ findNext();
+ return !finished && next != null;
+ }
+ @Override
+ public LedgerEntry next() {
+ // Just assume hasNext has been called
+ LedgerEntryItem item = (LedgerEntryItem) next.item;
+ return item.entry;
+ }
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public QualityIterator qualityIterator(final Hash256 bookBase) {
+ return new QualityIterator(bookBase);
+ }
+
+ public Iterable directoryIterator(OfferDirectory forQuality) {
+ // TODO: create an actual iterator
+ Vector256 indexes = new Vector256();
+ OfferDirectory cursor = forQuality;
+
+ while (cursor != null) {
+ indexes.addAll(cursor.indexes());
+ if (cursor.hasNextIndex()) {
+ LedgerEntry le = getLE(cursor.nextIndex());
+ if (le instanceof OfferDirectory) {
+ cursor = (OfferDirectory) le;
+ }
+ else {
+ break;
+ }
+ }
+ else {
+ break;
+ }
+ }
+ return indexes;
+ }
+
+ public void walkEntries(final LedgerEntryVisitor walker) {
+ walkLeaves(leaf -> {
+ LedgerEntryItem item = (LedgerEntryItem) leaf.item;
+ walker.onEntry(item.entry);
+ });
+ }
+
+ // TODO
+ public Hash256 getNextIndex(Hash256 nextIndex, Hash256 bookEnd) {
+ return null;
+ }
+
+ @Override
+ public AccountState copy() {
+ return (AccountState) super.copy();
+ }
+
+ public static AccountState loadFromLedgerDump(String filePath) {
+ try {
+ FileReader reader = new FileReader(filePath);
+ return loadFromLedgerDump(reader);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static AccountState loadFromLedgerDump(Reader reader) {
+ try {
+ JsonFactory jsonFactory = new JsonFactory();
+ JsonParser parser = jsonFactory.createParser(reader);
+ JsonToken jsonToken = parser.nextToken();
+ if (jsonToken != JsonToken.START_OBJECT) {
+ throw new AssertionError();
+ }
+
+ String accountState = "accountState";
+
+ // TODO:
+ while (parser.nextToken() != null) {
+ String currentName = parser.getCurrentName();
+ if (currentName.equals(accountState)) {
+ break;
+ }
+ }
+
+ if (!parser.getCurrentName().equals("accountState")) {
+ throw new IllegalStateException("No `accountState` field found!");
+ }
+
+ AccountState state = new AccountState();
+ ObjectMapper mapper = new ObjectMapper();
+ if (parser.nextToken() != JsonToken.START_ARRAY) {
+ throw new AssertionError();
+ }
+
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ TreeNode treeNode = mapper.readTree(parser);
+ state.addLE((LedgerEntry) STObject.fromJacksonObject((ObjectNode) treeNode));
+ }
+ return state;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountStateBuilder.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountStateBuilder.java
new file mode 100644
index 0000000000..1b43203db7
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/AccountStateBuilder.java
@@ -0,0 +1,320 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.fields.Field;
+import com.ripple.core.types.known.sle.LedgerEntry;
+import com.ripple.core.types.known.sle.ThreadedLedgerEntry;
+import com.ripple.core.types.known.sle.entries.*;
+import com.ripple.core.types.known.tx.result.AffectedNode;
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+import java.util.*;
+
+public class AccountStateBuilder {
+ private AccountState state;
+ private AccountState previousState = null;
+ private long targetLedgerIndex;
+ public long nextTransactionIndex = 0;
+ private Hash256 targetAccountHash;
+ public long totalTransactions = 0;
+
+ private TreeSet directoriesModifiedMoreThanOnceByTransaction = new TreeSet<>();
+ private TreeSet directoriesModifiedByTransaction = new TreeSet<>();
+ public TreeSet ledgerModifiedEntries = new TreeSet<>();
+ public TreeSet ledgerDeletedEntries = new TreeSet<>();
+
+ public boolean sortedDirectories = true;
+
+ public void resetModified() {
+ ledgerModifiedEntries.clear();
+ ledgerDeletedEntries.clear();
+ directoriesModifiedMoreThanOnceByTransaction.clear();
+ }
+
+ public AccountStateBuilder(AccountState state, long targetLedgerIndex) {
+ this.state = state;
+ setStateCheckPoint();
+ this.targetLedgerIndex = targetLedgerIndex;
+ }
+
+ public void onLedgerClose(long ledgerIndex, Hash256 accountHash, Hash256 parentHash) {
+ state.updateSkipLists(ledgerIndex, parentHash);
+ targetLedgerIndex = ledgerIndex;
+ targetAccountHash = accountHash;
+ nextTransactionIndex = 0;
+ }
+
+ public void setStateCheckPoint() {
+ previousState = state.copy();
+ }
+
+ public void onTransaction(TransactionResult tr) {
+ System.out.println("adding tx: " + tr.hash);
+ if (tr.meta.transactionIndex().longValue() != nextTransactionIndex) throw new AssertionError();
+ if (tr.ledgerIndex.longValue() != targetLedgerIndex + 1) throw new AssertionError(String.format("%d != %d", tr.ledgerIndex.longValue(), targetLedgerIndex + 1));
+ nextTransactionIndex++;
+ totalTransactions++;
+ directoriesModifiedByTransaction = new TreeSet<>();
+
+ for (AffectedNode an : sortedAffectedNodes(tr)) {
+ Hash256 id = an.ledgerIndex();
+ LedgerEntry le = an.nodeAsFinal();
+
+ if (an.isCreatedNode()) {
+ ledgerModifiedEntries.add(id);
+ ledgerDeletedEntries.remove(id);
+ le.setDefaults();
+ state.addLE(le);
+
+ if (le instanceof Offer) {
+ Offer offer = (Offer) le;
+ offer.setOfferDefaults(); // TODO / TODO
+
+ for (Hash256 directory : offer.directoryIndexes()) {
+ DirectoryNode dn = getDirectoryForUpdating(directory);
+ Hash256 index = offer.index();
+ addToDirectoryNode(dn, index);
+ }
+ } else if (le instanceof RippleState) {
+ RippleState state = (RippleState) le;
+
+ for (Hash256 directory : state.directoryIndexes()) {
+ DirectoryNode dn = getDirectoryForUpdating(directory);
+ addToDirectoryNode(dn, state.index());
+ }
+ }
+
+ if (le instanceof IndexedLedgerEntry) {
+ IndexedLedgerEntry owned = (IndexedLedgerEntry) le;
+ for (Hash256 directory : owned.ownerDirectoryIndexes(tr.txn)) {
+ DirectoryNode dn = getDirectoryForUpdating(directory);
+ addToDirectoryNode(dn, owned.index());
+ }
+ }
+
+ if (le instanceof ThreadedLedgerEntry) {
+ ThreadedLedgerEntry tle = (ThreadedLedgerEntry) le;
+ tle.previousTxnID(tr.hash);
+ tle.previousTxnLgrSeq(tr.ledgerIndex);
+ }
+ } else if (an.isDeletedNode()) {
+ ledgerModifiedEntries.remove(id);
+ ledgerDeletedEntries.add(id);
+ directoriesModifiedMoreThanOnceByTransaction.remove(id);
+ state.removeLeaf(id);
+ if (le instanceof Offer) {
+ Offer offer = (Offer) le;
+ for (Hash256 directory : offer.directoryIndexes()) {
+ DirectoryNode dn = getDirectoryForUpdating(directory);
+ if (dn != null) {
+// Hash256 index = offer.index();
+ if (dn.owner() != null) {
+ deleteFromDirectoryUnstable(offer, dn);
+ } else {
+ deleteFromDirectoryStable(offer, dn);
+ }
+ }
+ }
+ } else if (le instanceof RippleState) {
+ RippleState state = (RippleState) le;
+ for (Hash256 directory : state.directoryIndexes()) {
+ DirectoryNode dn = getDirectoryForUpdating(directory);
+ if (dn != null) {
+ deleteFromDirectoryUnstable(le, dn);
+ }
+ }
+ } else if (le instanceof IndexedLedgerEntry) {
+ IndexedLedgerEntry owned = (IndexedLedgerEntry) le;
+ for (Hash256 directory : owned.ownerDirectoryIndexes(tr.txn)) {
+ DirectoryNode dn = getDirectoryForUpdating(directory);
+ deleteFromDirectoryUnstable(le, dn);
+ }
+ }
+ } else if (an.isModifiedNode()) {
+ ledgerModifiedEntries.add(id);
+ ShaMapLeaf leaf = state.getLeafForUpdating(id);
+ LedgerEntryItem item = (LedgerEntryItem) leaf.item;
+ LedgerEntry leModded = item.entry;
+
+ if (le instanceof ThreadedLedgerEntry) {
+ ThreadedLedgerEntry tle = (ThreadedLedgerEntry) le;
+ tle.previousTxnID(tr.hash);
+ tle.previousTxnLgrSeq(tr.ledgerIndex);
+ }
+ for (Field field : le) {
+ // Already have the `index`
+ if (field == Field.LedgerIndex) {
+ continue;
+ }
+ leModded.put(field, le.get(field));
+ }
+ // Find all removed fields
+ ArrayList removed = new ArrayList<>();
+ for (Field field : leModded) {
+ if (an.removedField(field)) {
+ removed.add(field);
+ }
+ }
+ removed.forEach(leModded::remove);
+ }
+ }
+ }
+
+ private void deleteFromDirectoryUnstable(LedgerEntry b4, DirectoryNode dn) {
+ boolean b = directoryRemoveUnstable(dn, b4.index());
+ DirectoryNode cursor = dn;
+
+ if (!b) {
+ while (cursor.indexNext() != null) {
+ cursor = getDirectoryForUpdating(cursor.nextIndex());
+ b = directoryRemoveUnstable(cursor, b4.index());
+ if (b) {
+ break;
+ }
+ }
+ }
+//
+ if (!b) {
+ cursor = dn;
+ while (cursor.indexPrevious() != null) {
+ cursor = getDirectoryForUpdating(cursor.prevIndex());
+ b = directoryRemoveUnstable(cursor, b4.index());
+ if (b) {
+ break;
+ }
+ }
+ }
+ }
+ private void deleteFromDirectoryStable(LedgerEntry b4, DirectoryNode dn) {
+ boolean b = directoryRemoveStable(dn, b4.index());
+ DirectoryNode cursor = dn;
+
+ if (!b) {
+ while (cursor.hasIndexPrevious()) {
+ cursor = getDirectoryForUpdating(cursor.prevIndex());
+ b = directoryRemoveStable(cursor, b4.index());
+ if (b) {
+ break;
+ }
+ }
+ }
+ if (!b) {
+ while (cursor.indexNext() != null) {
+ cursor = getDirectoryForUpdating(cursor.nextIndex());
+ b = directoryRemoveStable(cursor, b4.index());
+ if (b) {
+ break;
+ }
+ }
+ }
+ }
+
+ public static Collection makeCollection(Iterable iter) {
+ Collection list = new ArrayList<>();
+ for (E item : iter) {
+ list.add(item);
+ }
+ return list;
+ }
+ private ArrayList sortedAffectedNodes(TransactionResult tr) {
+ ArrayList sorted = new ArrayList<>(makeCollection(tr.meta.affectedNodes()));
+ sorted.sort(Comparator.comparingInt(this::getOrdinal));
+ return sorted;
+ }
+
+ private int getOrdinal(AffectedNode o1) {
+ switch (o1.ledgerEntryType()) {
+ case DirectoryNode:
+ return 10;
+ case RippleState:
+ return 20;
+ case Offer:
+ return 30;
+ case Escrow:
+ case SignerList:
+ case PayChannel:
+ return 21;
+ default:
+ return 40;
+ }
+ }
+
+ private void onDirectoryModified(DirectoryNode dn) {
+ Hash256 index = dn.index();
+ if (directoriesModifiedByTransaction.contains(index)) {
+ directoriesModifiedMoreThanOnceByTransaction.add(index);
+ }
+ else {
+ directoriesModifiedByTransaction.add(index);
+ }
+ }
+ private boolean directoryRemoveStable(DirectoryNode dn, Hash256 index) {
+ onDirectoryModified(dn);
+ return dn.indexes().remove(index);
+ }
+ private boolean directoryRemoveUnstable(DirectoryNode dn, Hash256 index) {
+ onDirectoryModified(dn);
+ if (sortedDirectories) {
+ return dn.indexes().remove(index);
+ } else {
+ return dn.indexes().removeUnstable(index);
+ }
+ }
+ private void addToDirectoryNode(DirectoryNode dn, Hash256 index) {
+ onDirectoryModified(dn);
+ if (dn.exchangeRate() != null) {
+ dn.indexes().add(index);
+ } else {
+ dn.indexes().add(index);
+ if (sortedDirectories) {
+ Collections.sort(dn.indexes());
+ }
+ }
+ }
+ private DirectoryNode getDirectoryForUpdating(Hash256 directoryIndex) {
+ ShaMapLeaf leaf = state.getLeafForUpdating(directoryIndex);
+ if (leaf == null) {
+ return null;
+ }
+ LedgerEntryItem lei = (LedgerEntryItem) leaf.item;
+ return (DirectoryNode) lei.entry;
+ }
+
+ public AccountState state() {
+ return state;
+ }
+
+ public long currentLedgerIndex() {
+ return targetLedgerIndex;
+ }
+
+ public String targetAccountHashHex() {
+ return targetAccountHash.toHex();
+ }
+ public Hash256 targetAccountHash() {
+ return targetAccountHash;
+ }
+
+ public TreeSet directoriesWithIndexesOutOfOrder() {
+ TreeSet ret = new TreeSet<>();
+ for (Hash256 hash256 : directoriesModifiedMoreThanOnceByTransaction) {
+ DirectoryNode dn = state.getDirectoryNode(hash256);
+ if (dn.owner() != null) {
+ ret.add(hash256);
+ }
+ }
+ return ret;
+ }
+
+ public boolean bad() {
+ return !state.hash().equals(targetAccountHash);
+ }
+
+ public AccountState previousState() {
+ return previousState;
+ }
+
+ public void setState(AccountState map) {
+ state = map;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/BytesItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/BytesItem.java
new file mode 100644
index 0000000000..11365302b5
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/BytesItem.java
@@ -0,0 +1,37 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BytesSink;
+
+public class BytesItem extends ShaMapItem {
+ private byte[] item;
+
+ public BytesItem(byte[] item) {
+ this.item = item;
+ }
+
+ @Override
+ void toBytesSink(BytesSink sink) {
+ sink.add(item);
+ }
+
+ @Override
+ public ShaMapItem copy() {
+ return this;
+ }
+
+ @Override
+ public byte[] value() {
+ return item;
+ }
+
+ @Override
+ public Prefix hashPrefix() {
+ return new Prefix() {
+ @Override
+ public byte[] bytes() {
+ return new byte[0];
+ }
+ };
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/HashedTreeWalker.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/HashedTreeWalker.java
new file mode 100644
index 0000000000..a565baf19b
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/HashedTreeWalker.java
@@ -0,0 +1,8 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.Hash256;
+
+public interface HashedTreeWalker {
+ public void onLeaf(Hash256 h, ShaMapLeaf le);
+ public void onInner(Hash256 h, ShaMapInner inner);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/LeafWalker.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/LeafWalker.java
new file mode 100644
index 0000000000..71f371cebe
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/LeafWalker.java
@@ -0,0 +1,5 @@
+package com.ripple.core.types.shamap;
+
+public interface LeafWalker {
+ void onLeaf(ShaMapLeaf shaMapLeaf);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryItem.java
new file mode 100644
index 0000000000..ea67d4bf0b
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryItem.java
@@ -0,0 +1,45 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.STObject;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.fields.Field;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+public class LedgerEntryItem extends ShaMapItem {
+ public LedgerEntryItem(LedgerEntry entry) {
+ this.entry = entry;
+ }
+
+ public LedgerEntry entry;
+
+ @Override
+ void toBytesSink(BytesSink sink) {
+ entry.toBytesSink(sink);
+ }
+
+ @Override
+ public String toString() {
+ return entry.prettyJSON();
+ }
+
+ @Override
+ public ShaMapItem copy() {
+ STObject object = STObject.fromBytes(entry.toBytes());
+ LedgerEntry le = (LedgerEntry) object;
+ // TODO: what about other auxiliary (non serialized) fields
+ le.index(entry.index());
+ return new LedgerEntryItem(le);
+ }
+
+ @Override
+ public LedgerEntry value() {
+ return entry;
+ }
+
+ @Override
+ public Prefix hashPrefix() {
+ return HashPrefix.leafNode;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryVisitor.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryVisitor.java
new file mode 100644
index 0000000000..6ce639b552
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/LedgerEntryVisitor.java
@@ -0,0 +1,7 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.types.known.sle.LedgerEntry;
+
+public interface LedgerEntryVisitor {
+ public void onEntry(LedgerEntry entry);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/NodeStore.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/NodeStore.java
new file mode 100644
index 0000000000..8d1bd98eee
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/NodeStore.java
@@ -0,0 +1,64 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.HalfSha512;
+import com.ripple.core.coretypes.hash.Hash256;
+
+/**
+
+ * This is a toy implementation for illustrative purposes.
+ */
+public class NodeStore {
+ /**
+ * In ripple, all data is stored in a simple binary key/value database.
+ * The keys are 256 bit binary strings and the values are binary strings of
+ * arbitrary length.
+ */
+ public static interface KeyValueBackend {
+ void put(Hash256 key, byte[] content);
+ byte[] get(Hash256 key);
+ }
+
+ KeyValueBackend backend;
+ public NodeStore(KeyValueBackend backend) {
+ this.backend = backend;
+ }
+ /**
+ * All data stored is keyed by the hash of it's contents.
+ * Ripple uses the first 256 bits of a sha512 as it's 33 percent
+ * faster than using sha256.
+ *
+ * @return `key` used to store the content
+ */
+ private Hash256 storeContent(byte[] content) {
+ HalfSha512 hasher = new HalfSha512();
+ hasher.update(content);
+ Hash256 key = hasher.finish();
+ storeHashKeyedContent(key, content);
+ return key;
+ }
+
+ /**
+ * @param hash As ripple uses the `hash` of the contents as the
+ * NodeStore key, `hash` is pervasively used in lieu of
+ * the term `key`.
+ */
+ private void storeHashKeyedContent(Hash256 hash, byte[] content) {
+ // Note: The real nodestore actually prepends some metadata, which doesn't
+ // contribute to the hash.
+ backend.put(hash, content); // metadata + content
+ }
+
+ /**
+ * The complement to `set` api, which together form a simple public interface.
+ */
+ public byte[] get(Hash256 hash) {
+ return backend.get(hash);
+
+ }
+ /**
+ * The complement to `get` api, which together form a simple public interface.
+ */
+ public Hash256 set(byte[] content) {
+ return storeContent(content);
+ }
+}
\ No newline at end of file
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/PathToIndex.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/PathToIndex.java
new file mode 100644
index 0000000000..dcc0dfd56e
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/PathToIndex.java
@@ -0,0 +1,156 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.Hash256;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+public class PathToIndex {
+ public Hash256 index;
+ public ShaMapLeaf leaf;
+
+ private ArrayDeque inners;
+ private ShaMapInner[] dirtied;
+ private boolean matched = false;
+
+ public boolean hasLeaf() {
+ return leaf != null;
+ }
+ public boolean leafMatchedIndex() {
+ return matched;
+ }
+ public boolean copyLeafOnUpdate() {
+ return leaf.version != dirtied[0].version;
+ }
+
+ int size() {
+ return inners.size();
+ }
+
+ public ShaMapInner top() {
+ return dirtied[dirtied.length - 1];
+ }
+
+ // returns the
+ public ShaMapInner dirtyOrCopyInners() {
+ if (maybeCopyOnWrite()) {
+ int ix = 0;
+ // We want to make a uniformly accessed array of the inners
+ dirtied = new ShaMapInner[inners.size()];
+ // from depth 0 to 1, to 2, to 3, don't be fooled by the api
+ Iterator it = inners.descendingIterator();
+
+ // This is actually the root which COULD be the top of the stack
+ // Think about it ;)
+ ShaMapInner top = it.next();
+ dirtied[ix++] = top;
+ top.invalidate();
+
+ while (it.hasNext()) {
+ ShaMapInner next = it.next();
+ boolean doCopies = next.version != top.version;
+
+ if (doCopies) {
+ ShaMapInner copy = next.copy(top.version);
+ copy.invalidate();
+ top.setBranch(index, copy);
+ next = copy;
+ } else {
+ next.invalidate();
+ }
+ top = next;
+ dirtied[ix++] = top;
+ }
+ return top;
+ } else {
+ copyInnersToDirtiedArray();
+ return inners.peekFirst();
+ }
+ }
+
+ public boolean hasMatchedLeaf() {
+ return hasLeaf() && leafMatchedIndex();
+ }
+
+ public void collapseOnlyLeafChildInners() {
+ assert dirtied != null;
+
+ ShaMapInner next;
+ ShaMapLeaf onlyChild = null;
+
+ for (int i = dirtied.length - 1; i >= 0; i--) {
+ next = dirtied[i];
+ if (onlyChild != null) {
+ next.setLeaf(onlyChild);
+ }
+ onlyChild = next.onlyChildLeaf();
+ if (onlyChild == null) {
+ break;
+ }
+ }
+ }
+
+ private void copyInnersToDirtiedArray() {
+ int ix = 0;
+ dirtied = new ShaMapInner[inners.size()];
+ Iterator descending = inners.descendingIterator();
+ while (descending.hasNext()) {
+ ShaMapInner next = descending.next();
+ dirtied[ix++] = next;
+ next.invalidate();
+ }
+ }
+
+ private boolean maybeCopyOnWrite() {
+ return inners.peekLast().doCoW;
+ }
+
+ public PathToIndex(ShaMapInner root, Hash256 index) {
+ this.index = index;
+ makeStack(root, index);
+ }
+
+ private void makeStack(ShaMapInner root, Hash256 index) {
+ inners = new ArrayDeque<>();
+ ShaMapInner top = root;
+
+ while (true) {
+ inners.push(top);
+ ShaMapNode existing = top.getBranch(index);
+ if (existing == null) {
+ break;
+ } else if (existing.isLeaf()) {
+ leaf = existing.asLeaf();
+ matched = leaf.index.equals(index);
+ break;
+ }
+ else if (existing.isInner()) {
+ top = existing.asInner();
+ }
+ }
+ }
+
+ public ShaMapLeaf invalidatedPossiblyCopiedLeafForUpdating() {
+ assert matched;
+ if (dirtied == null) {
+ dirtyOrCopyInners();
+ }
+ ShaMapLeaf theLeaf = leaf;
+
+ if (copyLeafOnUpdate()) {
+ theLeaf = leaf.copy();
+ top().setLeaf(theLeaf);
+ }
+ theLeaf.invalidate();
+ return theLeaf;
+ }
+
+ public ArrayList topDownList() {
+ ArrayList path = new ArrayList<>();
+ Iterator shaMapInnerIterator = inners.descendingIterator();
+ shaMapInnerIterator.forEachRemaining(path::add);
+ path.add(leaf);
+ return path;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/README.md b/ripple-core/src/main/java/com/ripple/core/types/shamap/README.md
new file mode 100644
index 0000000000..a78bc3ad73
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/README.md
@@ -0,0 +1,319 @@
+Rippled NodeStore
+-----------------
+
+To understand a ShaMap first you must know about the NodeStore.
+
+```java
+/**
+
+ * This is a toy implementation for illustrative purposes.
+ */
+public class NodeStore {
+ /**
+ * In ripple, all data is stored in a simple binary key/value database.
+ * The keys are 256 bit binary strings and the values are binary strings of
+ * arbitrary length.
+ */
+ public interface KeyValueBackend {
+ void put(Hash256 key, byte[] content);
+ byte[] get(Hash256 key);
+ }
+
+ KeyValueBackend backend;
+ public NodeStore(KeyValueBackend backend) {
+ this.backend = backend;
+ }
+ /**
+ * All data stored is keyed by the hash of it's contents.
+ * Ripple uses the first 256 bits of a sha512 as it's 33 percent
+ * faster than using sha256.
+ *
+ * @return `key` used to store the content
+ */
+ private Hash256 storeContent(byte[] content) {
+ Hash256.HalfSha512 hasher = new Hash256.HalfSha512();
+ hasher.update(content);
+ Hash256 key = hasher.finish();
+ storeHashKeyedContent(key, content);
+ return key;
+ }
+
+ /**
+ * @param hash As ripple uses the `hash` of the contents as the
+ * NodeStore key, `hash` is pervasively used in lieu of
+ * the term `key`.
+ */
+ private void storeHashKeyedContent(Hash256 hash, byte[] content) {
+ // Note: The real NodeStore actually prepends some metadata, which doesn't
+ // contribute to the hash.
+ backend.put(hash, content); // metadata + content
+ }
+
+ /**
+ * The complement to `set` api, which together form a simple public interface.
+ */
+ public byte[] get(Hash256 hash) {
+ return backend.get(hash);
+
+ }
+ /**
+ * The complement to `get` api, which together form a simple public interface.
+ */
+ public Hash256 set(byte[] content) {
+ return storeContent(content);
+ }
+}
+```
+
+See also:
+* [Serialized Types](../../../../../../../../README.md)
+* [BinaryFormats.txt (historical)](https://github.com/ripple/rippled/blob/07df5f1f81b0ee1ab641d134ba8e940a90f5297e/BinaryFormats.txt#L2-L6)
+
+Excerpt from BinaryFormats.txt (historical):
+
+
+ All signed or hashed objects must have well-defined binary formats at the
+ byte level. These formats do not have to be the same as the network wire
+ formats or the forms used for efficient storage or human display. However,
+ it must always be possible to precisely re-create, byte for byte, any signed
+ or hashed object. Otherwise, the signatures or hashes cannot be validated.
+
+
+Note that currently (26/Jan/2018) the NodeStore stores it in the hashing form.
+
+What is a ShaMap?
+-----------------
+
+A ShaMap is a special type of tree, used as a way to index values stored in a
+`NodeStore`
+
+Recall that values in the NodeStore are keyed by the hash of their contents.
+
+But what about identities that change over time? How can you retrieve a certain
+version of something? What could be used as an enduring identifier? The value
+must have some component[s] that are static over time. These are fed into a
+hashing function to create a 256 bit identifier.
+
+But how is this used? You can only query values by `hash` in the NodeStore. The
+hash, as a function of a value would obviously change along with it.
+
+The identifier is used as an `index` into a ShaMap tree, which in ripple, is
+representative of a point in time. In fact a ShaMap can be hashed
+deterministically, thus a point in time can be identified by a `hash`. Where is
+a ShaMap actually stored? In the NodeStore, of course.
+
+But the NodeStore only stores binary content you protest! But the ShaMap has a
+binary representation! So what are the `contents` of a ShaMap, to be hashed to
+be stored in the NodeStore?
+
+Glad you asked. A tree, has a root, and many children, either more branches, or
+terminal leaves. The root, and any of its children that have children
+themselves, are classed as `inner nodes`.
+
+In a ShaMap these `inner nodes` each have 16 `slots` for children. The binary
+representation of these is simply 16 `hash`es, used to retrieve child nodes from
+the NodeStore.
+
+An example of an inner node's `contents`
+
+ Empty slots are represented as 32 0 bytes
+
+ ```
+ 022CC592F5D4ABC3A63DA2A036CDDC0825B30717C78EF287BEF200056133FDA2
+ 0000000000000000000000000000000000000000000000000000000000000000
+ BEE626551799DDFE65BD2D9A0F0EA24D72C93CFD8E083176718D2B079EC60214
+ E1B34F1D9209CB668A50CCEE71C8109D140A6D715D923AEE98E6D53015D8B66B
+ 4C27A856094CFDE37CD2A0EA93DADB595B10CFEC55F816C987A6AC48D13AF5C0
+ 2F770714A9EF92792F44AA1537C18F68AFE3FFF157FB9088FFE2BDA695C19B71
+ C915CA982310CF41CF1266AA43C3B31ACBF4304D05ADB54A352D942C890763A3
+ F29FAD442CE204513BEA555A4192E324407444D946449CEA510C37A9BB982134
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 44B3BE10744EA2DA010D530C6AE64E3C3984DA7701EE79A66EA429EC11B87D1D
+ 0C7BE8569E9F08BADDEB91EBE79E5B98BBF245B067B7B83B4A95430CFEC9F7E8
+ BB38EA169DB6A020EA820BD1242DDB6250B397A26015507BA3C7F3C041EA683C
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 15B98934D22B5CB7233C42CE8DC8DD0D2328AB91CC574332C45D7160BD31D4AD
+ 1894E389AE4A63BA99C2D0546A58A976ECDAB14C09B98F532999B464696E29E5
+ ```
+
+What about those `index` thingies again? Remember, the `index` can't be used to
+query something directly in the NodeStore.
+
+First you need a known point in time, which we learned could be defined by a
+`hash` of a ShaMap.
+
+* shamap hash: 2F049AEE51C7C96AB911AF86E3278F4F90A38D196422A832617FF0C6F29C3704
+* value index: FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB
+
+Imagine we have those above. First we query the NodeStore with `2F049A...`
+
+What do we expect back? A ShaMap hash is the hash of the binary representation
+of the root node (which is an `inner node`) of the tree, so we'd expect
+something in the form shown earlier, with 16 256 bit hashes.
+
+From the NodeStore we retrieve:
+
+ ```
+ 4D494E00
+ 3C926652404076CA35EA1D89580975C50DCD0B29EC16079F168BE63BB1D6F237
+ F0D3F178C56C438C597D53FF5E3D44EE3865C17A0D25DCC2460F20BE4BEB4B7E
+ D65D6CA291E451E6D6566A5431D755A749D9B0450E83B376148A6F93349ACD46
+ 309B18093008055BE23E118DA4243DFC9AEF66E3260E2008BD6E6E6806F06725
+ 0000000000000000000000000000000000000000000000000000000000000000
+ D7D5A7C89DAE6D7517EDCB0601829CB339376EECD1DE8CA0216B6C18FFD77C32
+ 7C33AEB525B09A7BC013C01015D59A88129A8A6C18B45E6D7D0CA1F13E6FCB44
+ AB35119A7529D4BA57C41D7020833A0E9600895F61E9E9DEABBFDF82A877CBB2
+ 3BE7A48570263034E6DCBEFA6ED57B63C0592AE145341A79DAF57C3BDF7C5793
+ 17006AD967A1536E0441517990AD5B42890A3168AC9E045EAF9AC5A06F672561
+ 94B12B8BB76D525DD9320AF51893A6D69FF730C1E647181E7A7B51FE1FBF369E
+ 0579A72B3209AE66840F6E8317F422F43937E6AE1A14C8B30243DE016551643F
+ B873AA4EF1CDF0332C59BE423256610C4C6A1D32E2A1F8DE748390E29E8FC2D3
+ EB03A93BBC95ED56FB73E6FBDAB8F6916BBD1BD358336CBA4DA673558F5B068C
+ E64A78A07CF33C3B74AEA9862B5758513677324D4C9D72C69D14B4EB64D4EB02
+ 4E8C0CE75693B85A4CAC5516B77702E660A55281CF17DBE09552941F3078D81C
+ ```
+
+We see the hashes for 16 nodes clear as day, but what is this `4D494E00`
+prefixed to the front? Converted to `ascii` letters the hex `4D494E00` is
+`MIN\x00`, meaning sham)ap i)nner n)ode.
+
+The prefix `namespaces` the content, so different classes of objects which
+would otherwise have the same binary representation, will have a different
+`hash`. These `hash prefixes` serve another useful purpose, as we'll see later.
+(Similarly, there are namespacing prefixes for an `index` (created by
+ feeding static components of an identity into a hashing function))
+
+Is our value `index` hash amongst those enumerated? No !!! So what do we do with
+it? An index, usually means an ordinal, defining a place in an array. The
+`index` is actually an index into 64 arrays. Each nibble in the `index` is an
+index into the 16 slots in each inner node.
+
+Consider again the value `index`:
+
+ `FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB`
+
+To use the `index` we take the first nibble, `F` (yes, we go left to right)
+
+The letter `F` in hex has the ordinal value 15, so we take the 16th branch (0
+based indexing)
+
+We select the 16th hash
+
+ `4E8C0CE75693B85A4CAC5516B77702E660A55281CF17DBE09552941F3078D81C`
+
+From the NodeStore we retrieve:
+
+ ```
+ 4D494E00
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 286BC64A4A369857E4B0B5834C54CED797110D06E64F7759FBA2C87D3630D418
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 0000000000000000000000000000000000000000000000000000000000000000
+ 60631B91674954D21314B89CEE3B7F740B4FB1374610498EA1A7A3B79BA6706D
+ 3B1C36A6311256FE1E72BEDBA2042D67D86F561E51B387CFA680BCE9982CD6DB
+ 5568E7032EE018CA484C181CD68411B623229D0666AE0038708500C59402A282
+ 0000000000000000000000000000000000000000000000000000000000000000
+ ```
+
+There's that 'MIN\x00` hash prefix again.
+
+In fact, this prefix is how we can deterministically say that this is an
+`inner node` and that we can interpret the following bytes as 16 more
+`hash`es.
+
+We have descended deeper into the tree, but it seems we need to go deeper. We
+are currently at a depth of 2, so to go deeper we need the 2nd nibble.
+
+value index:
+ ```
+ FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB`
+ |
+ \
+ 2nd nibble
+ ```
+
+The letter `E` in hex has the ordinal value 14, so we take the 15th branch (0
+based indexing)
+
+We select the 15th hash:
+
+ `5568E7032EE018CA484C181CD68411B623229D0666AE0038708500C59402A282`
+
+From the NodeStore we retrieve:
+
+ ```
+ 534E4400
+ C11C12000722800000002400AA7BFF201900AA7BFC201B0226FF9464D4461B5C
+ A191A906000000000000000000000000455448000000000006CC4A6D023E68AA
+ 3499C6DE3E9F2DC52B8BA25465400000000861014A6840000000000000C17321
+ 03CF1DFB34A96363FF2B91638FCE51E6D7B88419729E9A81ABC99A9512FFB9C7
+ 3374463044022005ED4635AE246A4060378D9396CAA0E42F7E9129D309B2E98B
+ DF5CE2352520A002207D4494019EA4327F4087EA34B05CE9E5CD0AE3082BAD77
+ CC3698CE5A59C21BD581140BEC53D0830ADCE9E372086E570809916C440E83C3
+ F4201C00000033F8E311006F561BF352EEDB9072286286A4BBC8C419C995A171
+ 0AA0CFA8B0D2B843F07D294F60E82400AA7BFF501090B86A84C7F7843673BCF8
+ 2E565E69498CAEF463F8055ABA4C04581E76C9270064D4461B5CA191A9060000
+ 00000000000000000000455448000000000006CC4A6D023E68AA3499C6DE3E9F
+ 2DC52B8BA25465400000000861014A81140BEC53D0830ADCE9E372086E570809
+ 916C440E83E1E1E31100645690B86A84C7F7843673BCF82E565E69498CAEF463
+ F8055ABA4C04581E76C92700E8364C04581E76C927005890B86A84C7F7843673
+ BCF82E565E69498CAEF463F8055ABA4C04581E76C92700011100000000000000
+ 00000000004554480000000000021106CC4A6D023E68AA3499C6DE3E9F2DC52B
+ 8BA254E1E1E41100645690B86A84C7F7843673BCF82E565E69498CAEF463F805
+ 5ABA4C045835BF30CCBFE72200000000364C045835BF30CCBF5890B86A84C7F7
+ 843673BCF82E565E69498CAEF463F8055ABA4C045835BF30CCBF011100000000
+ 00000000000000004554480000000000021106CC4A6D023E68AA3499C6DE3E9F
+ 2DC52B8BA2540311000000000000000000000000000000000000000004110000
+ 000000000000000000000000000000000000E1E1E411006F56AE4B62A73DC540
+ 40E523FCE863981AB601B6D6BCD3CFCE303B43A430FC6D4DB1E7220000000024
+ 00AA7BFC250226FF9133000000000000000034000000000000000055B61A418E
+ 421021CFBA22B02F4934B4F79B1091F91CF910AEB21DA0975CEC52AB501090B8
+ 6A84C7F7843673BCF82E565E69498CAEF463F8055ABA4C045835BF30CCBF64D4
+ 461B7D5C693EDF000000000000000000000000455448000000000006CC4A6D02
+ 3E68AA3499C6DE3E9F2DC52B8BA25465400000000861014A81140BEC53D0830A
+ DCE9E372086E570809916C440E83E1E1E511006456B937CC88FCAF18886CC8D4
+ B19A5326F56B0B84E06AE511407B072DE348E05376E722000000003100000000
+ 0000000032000000000000000058B937CC88FCAF18886CC8D4B19A5326F56B0B
+ 84E06AE511407B072DE348E0537682140BEC53D0830ADCE9E372086E57080991
+ 6C440E83E1E1E5110061250226FF915558E2CDAB1D8D5477D8FED6EA5EFD9F9F
+ D8FAC56EA907E94197900C6C6175CE9556E9293AF964F2B20467530673C8F327
+ 41ED03DEF52F6B6625B4D837C155856C26E62400AA7BFF6240000000B60EBC57
+ E1E722000000002400AA7C002D000000186240000000B60EBB9681140BEC53D0
+ 830ADCE9E372086E570809916C440E83E1E1F1031000
+ FEEE5CC92B64375C8FEE56D54A82B9965E44FE0DCF673DBF27D0AA93F8AFF4FB
+ ```
+
+Well, here's something new. The `hash prefix` is different. This time the
+hex decodes as `MLN\x00`, meaning sham)ap l)eaf n)ode.
+
+And what's that at the end? Is that our index? It is!!
+
+Why does it need to be stored? We have only used `FE` to traverse to this
+node. Without storing the `index` identifier in the leaf node contents,
+there would be no way to be certain that this leaf held the item you wanted.
+More importantly, it acts as further name-spacing, to prevent collisions.
+(Technically, you could synthesize the index, by parsing the contents of
+the object and recreating it)
+
+Takeaways
+---------
+
+* A `hash` keys the NodeStore
+* An `index` is a path to an item in a ShaMap
+* For communication purposes
+ - Always use `hash` when referring to a key for the NodeStore
+ - Always use `index` when referring to a key for a ShaMap
+
+Links
+-----
+
+* [Rippled Hash Prefix declarations](../../coretypes/hash/prefixes/HashPrefix.java)
\ No newline at end of file
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMap.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMap.java
new file mode 100644
index 0000000000..0129eb7c4a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMap.java
@@ -0,0 +1,30 @@
+package com.ripple.core.types.shamap;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ShaMap extends ShaMapInner {
+ private AtomicInteger copies;
+
+ public ShaMap() {
+ super(0);
+ // This way we can copy the first to the second,
+ // copy the second, then copy the first again ;)
+ copies = new AtomicInteger();
+ }
+ public ShaMap(boolean isCopy, int depth) {
+ super(isCopy, depth, 0);
+ }
+
+ @Override
+ protected ShaMapInner makeInnerOfSameClass(int depth) {
+ return new ShaMap(true, depth);
+ }
+
+ public ShaMap copy() {
+ version = copies.incrementAndGet();
+ ShaMap copy = (ShaMap) copy(copies.incrementAndGet());
+ copy.copies = copies;
+ return copy;
+ }
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapDiff.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapDiff.java
new file mode 100644
index 0000000000..41e23d50e1
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapDiff.java
@@ -0,0 +1,122 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.Hash256;
+
+import java.util.TreeSet;
+
+public class ShaMapDiff {
+ public ShaMap one, two;
+
+ public TreeSet modified = new TreeSet<>();
+ public TreeSet deleted = new TreeSet<>();
+ public TreeSet added = new TreeSet<>();
+
+ public ShaMapDiff(ShaMap one, ShaMap two) {
+ this.one = one;
+ this.two = two;
+ }
+
+ // Find what's added, modified and deleted in `two`
+ public ShaMapDiff find() {
+ one.hash();
+ two.hash();
+ compare(one, two);
+ return this;
+ }
+
+ public ShaMapDiff inverted() {
+ ShaMapDiff shaMapDiff = new ShaMapDiff(two, one);
+
+ shaMapDiff.added = deleted;
+ shaMapDiff.modified = modified;
+ shaMapDiff.deleted = added;
+
+ return shaMapDiff;
+ }
+
+ public void apply(ShaMap sa) {
+ for (Hash256 mod : modified) {
+ boolean modded = sa.updateItem(mod, two.getItem(mod).copy());
+ if (!modded) throw new AssertionError();
+ }
+
+ for (Hash256 add : added) {
+ boolean added = sa.addItem(add, two.getItem(add).copy());
+ if (!added) throw new AssertionError();
+ }
+ for (Hash256 delete : deleted) {
+ boolean removed = sa.removeLeaf(delete);
+ if (!removed) throw new AssertionError();
+ }
+ }
+ private void compare(ShaMapInner a, ShaMapInner b) {
+ for (int i = 0; i < 16; i++) {
+ ShaMapNode aChild = a.getBranch(i);
+ ShaMapNode bChild = b.getBranch(i);
+
+ if (aChild == null && bChild != null) {
+ trackAdded(bChild);
+ // added in B
+ } else if (aChild != null && bChild == null) {
+ trackRemoved(aChild);
+ // removed from B
+ } else if (aChild != null && !aChild.hash().equals(bChild.hash())) {
+ boolean aleaf = aChild.isLeaf(),
+ bLeaf = bChild.isLeaf();
+
+ if (aleaf && bLeaf) {
+ ShaMapLeaf la = (ShaMapLeaf) aChild;
+ ShaMapLeaf lb = (ShaMapLeaf) bChild;
+ if (la.index.equals(lb.index)) {
+ modified.add(la.index);
+ } else {
+ deleted.add(la.index);
+ added.add(lb.index);
+ }
+ } else if (aleaf /*&& bInner*/) {
+ ShaMapLeaf la = (ShaMapLeaf) aChild;
+ ShaMapInner ib = (ShaMapInner) bChild;
+ trackAdded(ib);
+
+ //noinspection Duplicates
+ if (ib.hasLeaf(la.index)) {
+ // because trackAdded would have added it
+ added.remove(la.index);
+ ShaMapLeaf leaf = ib.getLeaf(la.index);
+ if (!leaf.hash().equals(la.hash())) {
+ modified.add(la.index);
+ }
+ } else {
+ deleted.add(la.index);
+ }
+ } else if (bLeaf /*&& aInner*/) {
+ ShaMapLeaf lb = (ShaMapLeaf) bChild;
+ ShaMapInner ia = (ShaMapInner) aChild;
+ trackRemoved(ia);
+
+ //noinspection Duplicates
+ if (ia.hasLeaf(lb.index)) {
+ // because trackRemoved would have deleted it
+ deleted.remove(lb.index);
+ ShaMapLeaf leaf = ia.getLeaf(lb.index);
+ if (!leaf.hash().equals(lb.hash())) {
+ modified.add(lb.index);
+ }
+ } else {
+ added.add(lb.index);
+ }
+ } else /*if (aInner && bInner)*/ {
+ compare((ShaMapInner) aChild, (ShaMapInner) bChild);
+ }
+ }
+ }
+ }
+
+ private void trackRemoved(ShaMapNode child) {
+ child.walkAnyLeaves(leaf -> deleted.add(leaf.index));
+ }
+
+ private void trackAdded(ShaMapNode child) {
+ child.walkAnyLeaves(leaf -> added.add(leaf.index));
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapInner.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapInner.java
new file mode 100644
index 0000000000..f7d73dae65
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapInner.java
@@ -0,0 +1,331 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BytesSink;
+
+import java.util.Iterator;
+
+public class ShaMapInner extends ShaMapNode implements Iterable {
+ public int depth;
+ int slotBits = 0;
+ int version = 0;
+ boolean doCoW;
+ protected ShaMapNode[] branches = new ShaMapNode[16];
+
+ public ShaMapInner(int depth) {
+ this(false, depth, 0);
+ }
+
+ public ShaMapInner(boolean isCopy, int depth, int version) {
+ this.doCoW = isCopy;
+ this.depth = depth;
+ this.version = version;
+ }
+
+ protected ShaMapInner copy(int version) {
+ ShaMapInner copy = makeInnerOfSameClass(depth);
+ System.arraycopy(branches, 0, copy.branches, 0, branches.length);
+ copy.slotBits = slotBits;
+ copy.hash = hash;
+ copy.version = version;
+ doCoW = true;
+
+ return copy;
+ }
+
+ protected ShaMapInner makeInnerOfSameClass(int depth) {
+ return new ShaMapInner(true, depth, version);
+ }
+
+ protected ShaMapInner makeInnerChild() {
+ int childDepth = depth + 1;
+ if (childDepth >= 64) throw new AssertionError();
+ return new ShaMapInner(doCoW, childDepth, version);
+ }
+
+ // Descend into the tree, find the leaf matching this index
+ // and if the tree has it.
+ protected void setLeaf(ShaMapLeaf leaf) {
+ if (leaf.version == -1) {
+ leaf.version = version;
+ }
+ setBranch(leaf.index, leaf);
+ }
+
+ private void removeBranch(Hash256 index) {
+ removeBranch(selectBranch(index));
+ }
+
+ public void walkLeaves(LeafWalker leafWalker) {
+ for (ShaMapNode branch : branches) {
+ if (branch != null) {
+ if (branch.isInner()) {
+ branch.asInner().walkLeaves(leafWalker);
+ } else if (branch.isLeaf()) {
+ leafWalker.onLeaf(branch.asLeaf());
+ }
+ }
+ }
+ }
+
+ public void walkTree(TreeWalker treeWalker) {
+ treeWalker.onInner(this);
+ for (ShaMapNode branch : branches) {
+ if (branch != null) {
+ if (branch.isLeaf()) {
+ ShaMapLeaf ln = branch.asLeaf();
+ treeWalker.onLeaf(ln);
+ } else if (branch.isInner()) {
+ ShaMapInner childInner = branch.asInner();
+ childInner.walkTree(treeWalker);
+ }
+ }
+ }
+
+ }
+
+ public void walkHashedTree(HashedTreeWalker walker) {
+ walker.onInner(hash(), this);
+
+ for (ShaMapNode branch : branches) {
+ if (branch != null) {
+ if (branch.isLeaf()) {
+ ShaMapLeaf ln = branch.asLeaf();
+ walker.onLeaf(branch.hash(), ln);
+ } else if (branch.isInner()) {
+ ShaMapInner childInner = branch.asInner();
+ childInner.walkHashedTree(walker);
+ }
+ }
+ }
+ }
+
+ /**
+ * @return the `only child` leaf or null if other children
+ */
+ public ShaMapLeaf onlyChildLeaf() {
+ ShaMapLeaf leaf = null;
+ int leaves = 0;
+
+ for (ShaMapNode branch : branches) {
+ if (branch != null) {
+ if (branch.isInner()) {
+ leaf = null;
+ break;
+ } else if (++leaves == 1) {
+ leaf = branch.asLeaf();
+ } else {
+ leaf = null;
+ break;
+ }
+ }
+ }
+ return leaf;
+ }
+
+ public boolean removeLeaf(Hash256 index) {
+ PathToIndex path = pathToIndex(index);
+ if (path.hasMatchedLeaf()) {
+ ShaMapInner top = path.dirtyOrCopyInners();
+ top.removeBranch(index);
+ path.collapseOnlyLeafChildInners();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public ShaMapItem getItem(Hash256 index) {
+ ShaMapLeaf leaf = getLeaf(index);
+
+ @SuppressWarnings("unchecked")
+ ShaMapItem shaMapItem = leaf == null ? null : leaf.item;
+ return shaMapItem;
+ }
+
+ public boolean addItem(Hash256 index, ShaMapItem item) {
+ return addLeaf(new ShaMapLeaf(index, item));
+ }
+
+ public boolean updateItem(Hash256 index, ShaMapItem item) {
+ return updateLeaf(new ShaMapLeaf(index, item));
+ }
+
+ public boolean hasLeaf(Hash256 index) {
+ return pathToIndex(index).hasMatchedLeaf();
+ }
+
+ public ShaMapLeaf getLeaf(Hash256 index) {
+ PathToIndex stack = pathToIndex(index);
+ if (stack.hasMatchedLeaf()) {
+ return stack.leaf;
+ } else {
+ return null;
+ }
+ }
+
+ public boolean addLeaf(ShaMapLeaf leaf) {
+ PathToIndex stack = pathToIndex(leaf.index);
+ if (stack.hasMatchedLeaf()) {
+ return false;
+ } else {
+ ShaMapInner top = stack.dirtyOrCopyInners();
+ top.addLeafToTerminalInner(leaf);
+ return true;
+ }
+ }
+
+ public boolean updateLeaf(ShaMapLeaf leaf) {
+ PathToIndex stack = pathToIndex(leaf.index);
+ if (stack.hasMatchedLeaf()) {
+ ShaMapInner top = stack.dirtyOrCopyInners();
+ // Why not update in place? Because of structural sharing
+ top.setLeaf(leaf);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public PathToIndex pathToIndex(Hash256 index) {
+ return new PathToIndex(this, index);
+ }
+
+ /**
+ * This should only be called on the deepest inners, as it
+ * does not do any dirtying.
+ * @param leaf to add to inner
+ */
+ void addLeafToTerminalInner(ShaMapLeaf leaf) {
+ ShaMapNode branch = getBranch(leaf.index);
+ if (branch == null) {
+ setLeaf(leaf);
+ } else if (branch.isInner()) {
+ // This should never be called
+ throw new AssertionError();
+ } else if (branch.isLeaf()) {
+ ShaMapInner inner = makeInnerChild();
+ setBranch(leaf.index, inner);
+ inner.addLeafToTerminalInner(leaf);
+ inner.addLeafToTerminalInner(branch.asLeaf());
+ }
+ }
+
+ protected void setBranch(Hash256 index, ShaMapNode node) {
+ setBranch(selectBranch(index), node);
+ }
+
+ protected ShaMapNode getBranch(Hash256 index) {
+ return getBranch(index.nibblet(depth));
+ }
+
+ public ShaMapNode getBranch(int i) {
+ return branches[i];
+ }
+
+ public ShaMapNode branch(int i) {
+ return branches[i];
+ }
+
+ protected int selectBranch(Hash256 index) {
+ return index.nibblet(depth);
+ }
+
+ public boolean hasLeaf(int i) {
+ return branches[i].isLeaf();
+ }
+ public boolean hasInner(int i) {
+ return branches[i].isInner();
+ }
+ public boolean hasNone(int i) {return branches[i] == null;}
+
+ private void setBranch(int slot, ShaMapNode node) {
+ slotBits = slotBits | (1 << slot);
+ branches[slot] = node;
+ invalidate();
+ }
+
+ private void removeBranch(int slot) {
+ branches[slot] = null;
+ slotBits = slotBits & ~(1 << slot);
+ }
+ public boolean empty() {
+ return slotBits == 0;
+ }
+
+ @Override public boolean isLeaf() { return false; }
+ @Override public boolean isInner() { return true; }
+
+ @Override
+ Prefix hashPrefix() {
+ return HashPrefix.innerNode;
+ }
+
+ @Override
+ public void toBytesSink(BytesSink sink) {
+ for (ShaMapNode branch : branches) {
+ if (branch != null) {
+ branch.hash().toBytesSink(sink);
+ } else {
+ Hash256.ZERO_256.toBytesSink(sink);
+ }
+ }
+ }
+
+ @Override
+ public Hash256 hash() {
+ if (empty()) {
+ // empty inners have a hash of all ZERO
+ // it's only valid for a root node to be empty
+ // any other inner node, must contain at least a
+ // single leaf
+ assert depth == 0;
+ return Hash256.ZERO_256;
+ } else {
+ // hash the hashPrefix() and toBytesSink
+ return super.hash();
+ }
+ }
+
+ public ShaMapLeaf getLeafForUpdating(Hash256 leaf) {
+ PathToIndex path = pathToIndex(leaf);
+ if (path.hasMatchedLeaf()) {
+ return path.invalidatedPossiblyCopiedLeafForUpdating();
+ }
+ return null;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new Iterator() {
+ int ix = 0;
+
+ @Override
+ public boolean hasNext() {
+ return ix != 16;
+ }
+
+ @Override
+ public ShaMapNode next() {
+ return branch(ix++);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ public int branchCount() {
+ int populated = 0;
+ for (ShaMapNode branch : branches) {
+ if (branch != null) {
+ populated ++;
+ }
+ }
+ return populated;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapItem.java
new file mode 100644
index 0000000000..a8ec303dbb
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapItem.java
@@ -0,0 +1,11 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BytesSink;
+
+abstract public class ShaMapItem {
+ abstract void toBytesSink(BytesSink sink);
+ public abstract ShaMapItem copy();
+ public abstract T value();
+ public abstract Prefix hashPrefix();
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapLeaf.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapLeaf.java
new file mode 100644
index 0000000000..3a79dacb94
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapLeaf.java
@@ -0,0 +1,34 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BytesSink;
+
+public class ShaMapLeaf extends ShaMapNode {
+ public Hash256 index;
+ public ShaMapItem item;
+ public long version = -1;
+
+ protected ShaMapLeaf(Hash256 index, ShaMapItem item) {
+ this.index = index;
+ this.item = item;
+ }
+
+ @Override public boolean isLeaf() {return true;}
+ @Override public boolean isInner() {return false;}
+
+ @Override
+ Prefix hashPrefix() {
+ return item.hashPrefix();
+ }
+
+ @Override
+ public void toBytesSink(BytesSink sink) {
+ item.toBytesSink(sink);
+ index.toBytesSink(sink);
+ }
+
+ public ShaMapLeaf copy() {
+ return new ShaMapLeaf(index, item.copy());
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapNode.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapNode.java
new file mode 100644
index 0000000000..c5e6d00aca
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/ShaMapNode.java
@@ -0,0 +1,47 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.HalfSha512;
+import com.ripple.core.coretypes.hash.Hash256;
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BytesSink;
+
+abstract public class ShaMapNode {
+ protected Hash256 hash;
+
+ // This saves a lot of instanceof checks
+ public abstract boolean isLeaf();
+ public abstract boolean isInner();
+
+ public ShaMapLeaf asLeaf() {
+ return (ShaMapLeaf) this;
+ }
+ public ShaMapInner asInner() {
+ return (ShaMapInner) this;
+ }
+
+ abstract Prefix hashPrefix();
+ abstract public void toBytesSink(BytesSink sink);
+
+ public void invalidate() {hash = null;}
+ public Hash256 hash() {
+ if (hash == null) {
+ hash = createHash();
+ }
+ return hash;
+ }
+ private Hash256 createHash() {
+ HalfSha512 half = HalfSha512.prefixed256(hashPrefix());
+ toBytesSink(half);
+ return half.finish();
+ }
+ /**
+ * Walk any leaves, possibly this node itself, if it's terminal.
+ */
+ public void walkAnyLeaves(LeafWalker leafWalker) {
+ if (isLeaf()) {
+ leafWalker.onLeaf(asLeaf());
+ } else {
+ asInner().walkLeaves(leafWalker);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultItem.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultItem.java
new file mode 100644
index 0000000000..9e4d34a39d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultItem.java
@@ -0,0 +1,38 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
+import com.ripple.core.coretypes.hash.prefixes.Prefix;
+import com.ripple.core.serialized.BinarySerializer;
+import com.ripple.core.serialized.BytesSink;
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+public class TransactionResultItem extends ShaMapItem {
+ public TransactionResult result;
+
+ public TransactionResultItem(TransactionResult result) {
+ this.result = result;
+ }
+
+ @Override
+ void toBytesSink(BytesSink sink) {
+ BinarySerializer write = new BinarySerializer(sink);
+ write.addLengthEncoded(result.txn);
+ write.addLengthEncoded(result.meta);
+ }
+
+ @Override
+ public ShaMapItem copy() {
+ // that's ok right ;) these bad boys are immutable anyway
+ return this;
+ }
+
+ @Override
+ public TransactionResult value() {
+ return result;
+ }
+
+ @Override
+ public Prefix hashPrefix() {
+ return HashPrefix.txNode;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultVisitor.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultVisitor.java
new file mode 100644
index 0000000000..3993b3795d
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionResultVisitor.java
@@ -0,0 +1,6 @@
+package com.ripple.core.types.shamap;
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+public interface TransactionResultVisitor {
+ public void onTransaction(TransactionResult tx);
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionTree.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionTree.java
new file mode 100644
index 0000000000..72c4ac4ddc
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TransactionTree.java
@@ -0,0 +1,43 @@
+package com.ripple.core.types.shamap;
+
+import com.ripple.core.types.known.tx.result.TransactionResult;
+
+import java.util.TreeSet;
+
+public class TransactionTree extends ShaMap {
+ public TransactionTree() {
+ super();
+ }
+
+ public TransactionTree(boolean isCopy, int depth) {
+ super(isCopy, depth);
+ }
+
+ @Override
+ protected ShaMapInner makeInnerOfSameClass(int depth) {
+ return new TransactionTree(true, depth);
+ }
+
+ public void addTransactionResult(TransactionResult tr) {
+ TransactionResultItem item = new TransactionResultItem(tr);
+ addItem(tr.hash, item);
+ }
+
+ @Override
+ public TransactionTree copy() {
+ return (TransactionTree) super.copy();
+ }
+
+ public void walkTransactions(final TransactionResultVisitor walker) {
+ walkLeaves(leaf -> {
+ TransactionResultItem item = (TransactionResultItem) leaf.item;
+ walker.onTransaction(item.result);
+ });
+ }
+
+ public TreeSet toTreeSet() {
+ TreeSet result = new TreeSet<>();
+ walkTransactions(result::add);
+ return result;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/core/types/shamap/TreeWalker.java b/ripple-core/src/main/java/com/ripple/core/types/shamap/TreeWalker.java
new file mode 100644
index 0000000000..de225e9eaf
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/core/types/shamap/TreeWalker.java
@@ -0,0 +1,6 @@
+package com.ripple.core.types.shamap;
+
+public interface TreeWalker {
+ public void onLeaf(ShaMapLeaf leaf);
+ public void onInner(ShaMapInner inner);
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/Seed.java b/ripple-core/src/main/java/com/ripple/crypto/Seed.java
new file mode 100644
index 0000000000..5c0231828f
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/Seed.java
@@ -0,0 +1,85 @@
+package com.ripple.crypto;
+
+import com.ripple.crypto.ecdsa.K256;
+import com.ripple.crypto.ed25519.EDKeyPair;
+import com.ripple.crypto.keys.IKeyPair;
+import com.ripple.encodings.addresses.Addresses;
+import com.ripple.encodings.base58.B58;
+import com.ripple.utils.Sha512;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+public class Seed {
+ private final byte[] seedBytes;
+ private B58.Version version;
+
+ public Seed(byte[] seedBytes) {
+ this(Addresses.SEED_K256, seedBytes);
+ }
+
+ public Seed(B58.Version version, byte[] seedBytes) {
+ this.seedBytes = seedBytes;
+ this.version = version;
+ }
+
+ @Override
+ public String toString() {
+ return Addresses.encode(seedBytes, version);
+ }
+
+ public byte[] bytes() {
+ return seedBytes;
+ }
+
+ public B58.Version version() {
+ return version;
+ }
+
+ public Seed setEd25519() {
+ this.version = Addresses.SEED_ED25519;
+ return this;
+ }
+
+ public IKeyPair keyPair() {
+ return keyPair(0);
+ }
+
+ public IKeyPair rootKeyPair() {
+ return keyPair(-1);
+ }
+
+ public IKeyPair keyPair(int account) {
+ if (version == Addresses.SEED_ED25519 ||
+ Arrays.equals(version.bytes, Addresses.SEED_ED25519.bytes)) {
+ if (account != 0) throw new IllegalStateException();
+ return EDKeyPair.from128Seed(seedBytes);
+ } else {
+ return K256.createKeyPair(seedBytes, account);
+ }
+
+ }
+
+ public static Seed fromBase58(String b58) {
+ B58.Decoded decoded = Addresses.decodeSeed(b58);
+ return new Seed(decoded.version, decoded.payload);
+ }
+
+ public static Seed fromPassPhrase(String passPhrase) {
+ return new Seed(passPhraseToSeedBytes(passPhrase));
+ }
+
+ public static byte[] passPhraseToSeedBytes(String phrase) {
+ try {
+ return new Sha512(phrase.getBytes("utf-8")).finish128();
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static IKeyPair getKeyPair(String b58) {
+ return fromBase58(b58).keyPair();
+ }
+}
+
+
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/ECDSASignature.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/ECDSASignature.java
new file mode 100644
index 0000000000..ab61bfec30
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/ECDSASignature.java
@@ -0,0 +1,155 @@
+package com.ripple.crypto.ecdsa;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1Integer;
+import org.bouncycastle.asn1.DERSequenceGenerator;
+import org.bouncycastle.asn1.DLSequence;
+import org.bouncycastle.crypto.signers.ECDSASigner;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+
+public class ECDSASignature {
+ /** The two components of the signature. */
+ public BigInteger r, s;
+
+ /** Constructs a signature with the given components. */
+ public ECDSASignature(BigInteger r, BigInteger s) {
+ this.r = r;
+ this.s = s;
+ }
+
+ public static boolean isStrictlyCanonical(byte[] sig) {
+ return checkIsCanonical(sig, true);
+ }
+
+ public static boolean checkIsCanonical(byte[] sig, boolean strict) {
+ // Make sure signature is canonical
+ // To protect against signature morphing attacks
+
+ // Signature should be:
+ // <30> [ <02> ] [ <02> ]
+ // where
+ // 6 <= len <= 70
+ // 1 <= lenR <= 33
+ // 1 <= lenS <= 33
+
+ int sigLen = sig.length;
+
+ if ((sigLen < 8) || (sigLen > 72))
+ return false;
+
+ if ((sig[0] != 0x30) || (sig[1] != (sigLen - 2)))
+ return false;
+
+ // Find R and check its length
+ int rPos = 4, rLen = sig[rPos - 1];
+
+ if ((rLen < 1) || (rLen > 33) || ((rLen + 7) > sigLen))
+ return false;
+
+ // Find S and check its length
+ int sPos = rLen + 6, sLen = sig[sPos - 1];
+ if ((sLen < 1) || (sLen > 33) || ((rLen + sLen + 6) != sigLen))
+ return false;
+
+ if ((sig[rPos - 2] != 0x02) || (sig[sPos - 2] != 0x02))
+ return false; // R or S have wrong type
+
+ if ((sig[rPos] & 0x80) != 0)
+ return false; // R is negative
+
+ if ((sig[rPos] == 0) && rLen == 1)
+ return false; // R is zero
+
+ if ((sig[rPos] == 0) && ((sig[rPos + 1] & 0x80) == 0))
+ return false; // R is padded
+
+ if ((sig[sPos] & 0x80) != 0)
+ return false; // S is negative
+
+ if ((sig[sPos] == 0) && sLen == 1)
+ return false; // S is zero
+
+ if ((sig[sPos] == 0) && ((sig[sPos + 1] & 0x80) == 0))
+ return false; // S is padded
+
+
+ byte[] rBytes = new byte[rLen];
+ byte[] sBytes = new byte[sLen];
+
+ System.arraycopy(sig, rPos, rBytes, 0, rLen);
+ System.arraycopy(sig, sPos, sBytes, 0, sLen);
+
+ BigInteger r = new BigInteger(1, rBytes), s = new BigInteger(1, sBytes);
+
+ BigInteger order = SECP256K1.order();
+
+ if (r.compareTo(order) != -1 || s.compareTo(order) != -1) {
+ return false; // R or S greater than modulus
+ }
+ if (strict) {
+ return order.subtract(s).compareTo(s) != -1;
+ } else {
+ return true;
+ }
+
+ }
+
+ public static ECDSASignature createSignature(byte[] hash, ECDSASigner signer) {
+ BigInteger[] sigs = signer.generateSignature(hash);
+ BigInteger r = sigs[0], s = sigs[1];
+
+ BigInteger otherS = SECP256K1.order().subtract(s);
+ if (s.compareTo(otherS) > 0) {
+ s = otherS;
+ }
+
+ return new ECDSASignature(r, s);
+ }
+
+ /**
+ * DER is an international standard for serializing data structures which is widely used in cryptography.
+ * It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding
+ * of the signature, as recognized by OpenSSL and other libraries.
+ */
+ public byte[] encodeToDER() {
+ try {
+ return derByteStream().toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e); // Cannot happen.
+ }
+ }
+
+ public static ECDSASignature decodeFromDER(byte[] bytes) {
+ try {
+ ASN1InputStream decoder = new ASN1InputStream(bytes);
+ DLSequence seq = (DLSequence) decoder.readObject();
+ ASN1Integer r, s;
+ try {
+ r = (ASN1Integer) seq.getObjectAt(0);
+ s = (ASN1Integer) seq.getObjectAt(1);
+ } catch (ClassCastException e) {
+ return null;
+ } finally {
+ decoder.close();
+ }
+ // OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
+ // Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html
+ return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected ByteArrayOutputStream derByteStream() throws IOException {
+ // Usually 70-72 bytes.
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(72);
+ DERSequenceGenerator seq = new DERSequenceGenerator(bos);
+ seq.addObject(new ASN1Integer(r));
+ seq.addObject(new ASN1Integer(s));
+ seq.close();
+ return bos;
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256.java
new file mode 100644
index 0000000000..1f93f23465
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256.java
@@ -0,0 +1,103 @@
+package com.ripple.crypto.ecdsa;
+
+import com.ripple.utils.Sha512;
+import com.ripple.utils.Utils;
+import org.bouncycastle.crypto.signers.ECDSASigner;
+import org.bouncycastle.math.ec.ECPoint;
+
+import java.math.BigInteger;
+
+public class K256 {
+
+ /**
+ * @param secretKey secret point on the curve as BigInteger
+ * @return corresponding public point
+ */
+ public static byte[] getPublic(BigInteger secretKey) {
+ return SECP256K1.basePointMultipliedBy(secretKey);
+ }
+
+ /**
+ * @param secretKey secret point on the curve as BigInteger
+ * @return corresponding public point
+ */
+ private static ECPoint computePublic(BigInteger secretKey) {
+ return SECP256K1.basePoint().multiply(secretKey);
+ }
+
+ /**
+ * @param privateGen secret point on the curve as BigInteger
+ * @return the corresponding public key is the public generator
+ * (aka public root key, master public key).
+ * return as byte[] for convenience.
+ */
+ private static ECPoint computePublicGenerator(BigInteger privateGen) {
+ return computePublic(privateGen);
+ }
+
+ private static BigInteger computePrivateGen(byte[] seedBytes) {
+ return generateKey(seedBytes, null);
+ }
+
+ public static byte[] computePublicKey(byte[] publicGenBytes,
+ int accountNumber) {
+ ECPoint rootPubPoint = SECP256K1.curve().decodePoint(publicGenBytes);
+ BigInteger scalar = generateKey(publicGenBytes, accountNumber);
+ ECPoint point = SECP256K1.basePoint().multiply(scalar);
+ ECPoint offset = rootPubPoint.add(point);
+ return offset.getEncoded(true);
+ }
+
+ private static BigInteger computeSecretKey(BigInteger privateGen,
+ byte[] publicGenBytes,
+ int accountNumber) {
+ return generateKey(publicGenBytes, accountNumber)
+ .add(privateGen).mod(SECP256K1.order());
+ }
+
+ /**
+ * @param seedBytes - a bytes sequence of arbitrary length which will be hashed
+ * @param discriminator - nullable optional uint32 to hash
+ * @return a number between [1, order -1] suitable as a private key
+ */
+ private static BigInteger generateKey(byte[] seedBytes, Integer discriminator) {
+ BigInteger key = null;
+ for (long i = 0; i <= 0xFFFFFFFFL; i++) {
+ Sha512 sha512 = new Sha512().add(seedBytes);
+ if (discriminator != null) {
+ sha512.addU32(discriminator);
+ }
+ sha512.addU32((int) i);
+ byte[] keyBytes = sha512.finish256();
+ key = Utils.uBigInt(keyBytes);
+ if (key.compareTo(BigInteger.ZERO) > 0 &&
+ key.compareTo(SECP256K1.order()) < 0) {
+ break;
+ }
+ }
+ return key;
+ }
+
+ static ECDSASignature createECDSASignature(byte[] hash, ECDSASigner signer) {
+ return ECDSASignature.createSignature(hash, signer);
+ }
+
+ public static K256KeyPair createKeyPair(byte[] seedBytes, int accountNumber) {
+ @SuppressWarnings("SpellCheckingInspection")
+ BigInteger priv;
+ BigInteger privateGen;
+ // The private generator (aka root private key, master private key)
+ privateGen = computePrivateGen(seedBytes);
+ ECPoint publicGen = computePublicGenerator(privateGen);
+ byte[] pubGenBytes = publicGen.getEncoded(true);
+
+ if (accountNumber == -1) {
+ // The root keyPair
+ return new K256KeyPair(privateGen, publicGen, pubGenBytes);
+ } else {
+ priv = computeSecretKey(privateGen, pubGenBytes, accountNumber);
+ ECPoint pub = computePublic(priv);
+ return new K256KeyPair(priv, pub, null);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256KeyPair.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256KeyPair.java
new file mode 100644
index 0000000000..e417c9d85c
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256KeyPair.java
@@ -0,0 +1,54 @@
+package com.ripple.crypto.ecdsa;
+
+import com.ripple.crypto.keys.IKeyPair;
+import com.ripple.utils.HashUtils;
+import com.ripple.utils.Utils;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+import org.bouncycastle.crypto.signers.ECDSASigner;
+import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
+import org.bouncycastle.math.ec.ECPoint;
+
+import java.math.BigInteger;
+
+public class K256KeyPair extends K256VerifyingKey implements IKeyPair {
+ private byte[] privateKey;
+ private ECPrivateKeyParameters privateKeyParameters;
+
+ K256KeyPair(BigInteger privateKey, ECPoint pub, byte[] pubEncoded) {
+ super(pub, pubEncoded);
+ this.privateKey = Utils.padTo256(privateKey.toByteArray());
+ privateKeyParameters = new ECPrivateKeyParameters(privateKey,
+ SECP256K1.params());
+ }
+
+ @Override
+ public byte[] privateKey() {
+ return privateKey;
+ }
+
+ @Override
+ public byte[] signMessage(byte[] message) {
+ byte[] hash = HashUtils.halfSha512(message);
+ return signHash(hash);
+ }
+
+ public byte[] signHash(byte[] bytes) {
+ ECDSASigner signer = newSigner();
+
+ ECDSASignature sig = K256.createECDSASignature(bytes, signer);
+ byte[] der = sig.encodeToDER();
+ if (!ECDSASignature.isStrictlyCanonical(der)) {
+ throw new IllegalStateException("Signature is not strictly canonical");
+ }
+ return der;
+ }
+
+ private ECDSASigner newSigner() {
+ ECDSASigner signer = new ECDSASigner(
+ new HMacDSAKCalculator(new SHA256Digest()));
+ signer.init(true, privateKeyParameters);
+ return signer;
+ }
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256VerifyingKey.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256VerifyingKey.java
new file mode 100644
index 0000000000..7681c12ef6
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/K256VerifyingKey.java
@@ -0,0 +1,55 @@
+package com.ripple.crypto.ecdsa;
+
+import com.ripple.crypto.keys.IVerifyingKey;
+import com.ripple.encodings.addresses.Addresses;
+import com.ripple.utils.HashUtils;
+import org.bouncycastle.crypto.params.ECPublicKeyParameters;
+import org.bouncycastle.crypto.signers.ECDSASigner;
+import org.bouncycastle.math.ec.ECPoint;
+
+public class K256VerifyingKey implements IVerifyingKey {
+ private final ECPublicKeyParameters keyParameters;
+ private final byte[] canonicalPublicKey;
+
+ K256VerifyingKey(ECPoint publicKey, byte[] publicKeyBytes) {
+ if (publicKeyBytes == null) {
+ publicKeyBytes = publicKey.getEncoded(true);
+ } else if (publicKey == null) {
+ publicKey = SECP256K1.curve().decodePoint(publicKeyBytes);
+ }
+ canonicalPublicKey = publicKeyBytes;
+ keyParameters = new ECPublicKeyParameters(
+ publicKey, SECP256K1.params());
+ }
+
+ private K256VerifyingKey(byte[] pubKeyBytes) {
+ this(null, pubKeyBytes);
+ }
+
+ @Override
+ public byte[] canonicalPubBytes() {
+ return canonicalPublicKey;
+ }
+
+ @Override
+ public boolean verify(byte[] message, byte[] signature) {
+ byte[] bytes = HashUtils.halfSha512(message);
+ return verifyHash(bytes, signature);
+ }
+
+ public boolean verifyHash(byte[] hash, byte[] signature) {
+ ECDSASigner signer = new ECDSASigner();
+ signer.init(false, keyParameters);
+ ECDSASignature sig = ECDSASignature.decodeFromDER(signature);
+ return sig != null && signer.verifySignature(hash, sig.r, sig.s);
+ }
+
+ public static K256VerifyingKey fromNodePublicKey(String node) {
+ byte[] pub = Addresses.decodeNodePublic(node);
+ return new K256VerifyingKey(pub);
+ }
+
+ public static K256VerifyingKey fromCanonicalPubBytes(byte[] bytes) {
+ return new K256VerifyingKey(bytes);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ecdsa/SECP256K1.java b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/SECP256K1.java
new file mode 100644
index 0000000000..5350a27f75
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ecdsa/SECP256K1.java
@@ -0,0 +1,42 @@
+package com.ripple.crypto.ecdsa;
+
+import org.bouncycastle.asn1.sec.SECNamedCurves;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.math.ec.ECCurve;
+import org.bouncycastle.math.ec.ECPoint;
+
+import java.math.BigInteger;
+
+public class SECP256K1 {
+ private static final ECDomainParameters ecParams;
+ private static final X9ECParameters params;
+
+ static {
+
+ params = SECNamedCurves.getByName("secp256k1");
+ ecParams = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH());
+ }
+
+ public static ECDomainParameters params() {
+ return ecParams;
+ }
+
+ public static BigInteger order() {
+ return ecParams.getN();
+ }
+
+
+ public static ECCurve curve() {
+ return ecParams.getCurve();
+ }
+
+ public static ECPoint basePoint() {
+ return ecParams.getG();
+ }
+
+ static byte[] basePointMultipliedBy(BigInteger secret) {
+ return basePoint().multiply(secret).getEncoded(true);
+ }
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ed25519/ED25519.java b/ripple-core/src/main/java/com/ripple/crypto/ed25519/ED25519.java
new file mode 100644
index 0000000000..71da658b0c
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ed25519/ED25519.java
@@ -0,0 +1,9 @@
+package com.ripple.crypto.ed25519;
+
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
+
+public class ED25519 {
+ static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable
+ .getByName("Ed25519");
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDKeyPair.java b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDKeyPair.java
new file mode 100644
index 0000000000..5d638617c3
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDKeyPair.java
@@ -0,0 +1,52 @@
+package com.ripple.crypto.ed25519;
+
+import com.ripple.crypto.keys.IKeyPair;
+import com.ripple.utils.HashUtils;
+import com.ripple.utils.Utils;
+import net.i2p.crypto.eddsa.EdDSAEngine;
+import net.i2p.crypto.eddsa.EdDSAPrivateKey;
+import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
+import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
+
+import java.math.BigInteger;
+import java.security.PrivateKey;
+
+public class EDKeyPair extends EDVerifyingKey implements IKeyPair {
+
+ private final EdDSAPrivateKeySpec keySpec;
+
+ private EDKeyPair(EdDSAPrivateKeySpec keySpec) {
+ super(new EdDSAPublicKeySpec(keySpec.getA(), ED25519.ed25519), null);
+ this.keySpec = keySpec;
+ }
+
+ public static EDKeyPair from256Seed(byte[] seedBytes) {
+ EdDSAPrivateKeySpec keySpec = new EdDSAPrivateKeySpec(seedBytes,
+ ED25519.ed25519);
+ return new EDKeyPair(keySpec);
+ }
+
+ public static EDKeyPair from128Seed(byte[] seedBytes) {
+ assert seedBytes.length == 16;
+ return from256Seed(HashUtils.halfSha512(seedBytes));
+ }
+
+ @Override
+ public byte[] privateKey() {
+ return keySpec.geta();
+ }
+
+ @Override
+ public byte[] signMessage(byte[] message) {
+ try {
+ EdDSAEngine sgr = new EdDSAEngine(sha512digest());
+ PrivateKey sKey = new EdDSAPrivateKey(keySpec);
+ sgr.initSign(sKey);
+ sgr.update(message);
+ return sgr.sign();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDVerifyingKey.java b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDVerifyingKey.java
new file mode 100644
index 0000000000..e0c9da8d07
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/ed25519/EDVerifyingKey.java
@@ -0,0 +1,58 @@
+package com.ripple.crypto.ed25519;
+
+import com.ripple.crypto.keys.IVerifyingKey;
+import net.i2p.crypto.eddsa.EdDSAEngine;
+import net.i2p.crypto.eddsa.EdDSAPublicKey;
+import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
+import org.bouncycastle.util.Arrays;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class EDVerifyingKey implements IVerifyingKey {
+ private final EdDSAPublicKey publicKey;
+ private final byte[] canonicalPubBytes;
+ @SuppressWarnings("FieldCanBeLocal")
+ private final byte[] ED_PREFIX = {(byte) 0xED};
+
+ EDVerifyingKey(EdDSAPublicKeySpec spec, byte[] pubBytes) {
+ if (pubBytes == null) {
+ pubBytes = spec.getA().toByteArray();
+ } else if (spec == null) {
+ spec = new EdDSAPublicKeySpec(pubBytes, ED25519.ed25519);
+ }
+ publicKey = new EdDSAPublicKey(spec);
+ canonicalPubBytes = Arrays.concatenate(ED_PREFIX, pubBytes);
+ }
+
+ MessageDigest sha512digest() {
+ try {
+ return MessageDigest.getInstance("SHA-512");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public byte[] canonicalPubBytes() {
+ return canonicalPubBytes;
+ }
+
+ @Override
+ public boolean verify(byte[] message, byte[] sigBytes) {
+ try {
+ EdDSAEngine sgr = new EdDSAEngine(sha512digest());
+ sgr.initVerify(publicKey);
+ sgr.update(message);
+ return sgr.verify(sigBytes);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static EDVerifyingKey fromCanonicalPubBytes(byte[] bytes) {
+ // Strip the 0xED prefix from the key
+ byte[] pubBytes = Arrays.copyOfRange(bytes, 1, bytes.length);
+ return new EDVerifyingKey(null, pubBytes);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/keys/IKeyPair.java b/ripple-core/src/main/java/com/ripple/crypto/keys/IKeyPair.java
new file mode 100644
index 0000000000..ab76848154
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/keys/IKeyPair.java
@@ -0,0 +1,6 @@
+package com.ripple.crypto.keys;
+
+public interface IKeyPair extends IVerifyingKey {
+ byte[] privateKey();
+ byte[] signMessage(byte[] message);
+}
diff --git a/ripple-core/src/main/java/com/ripple/crypto/keys/IVerifyingKey.java b/ripple-core/src/main/java/com/ripple/crypto/keys/IVerifyingKey.java
new file mode 100644
index 0000000000..1b1c6ed7d0
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/crypto/keys/IVerifyingKey.java
@@ -0,0 +1,29 @@
+package com.ripple.crypto.keys;
+
+
+import com.ripple.crypto.ecdsa.K256VerifyingKey;
+import com.ripple.crypto.ed25519.EDVerifyingKey;
+import com.ripple.encodings.common.B16;
+import com.ripple.utils.HashUtils;
+
+public interface IVerifyingKey {
+ default String canonicalPubHex() {
+ return B16.encode(canonicalPubBytes());
+ }
+
+ default byte[] id() {
+ return HashUtils.SHA256_RIPEMD160(canonicalPubBytes());
+ }
+
+ byte[] canonicalPubBytes();
+
+ boolean verify(byte[] message, byte[] sigBytes);
+
+ static IVerifyingKey from(byte[] bytes) {
+ if (bytes[0] == (byte) 0xED) {
+ return EDVerifyingKey.fromCanonicalPubBytes(bytes);
+ } else {
+ return K256VerifyingKey.fromCanonicalPubBytes(bytes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ripple-core/src/main/java/com/ripple/encodings/addresses/Addresses.java b/ripple-core/src/main/java/com/ripple/encodings/addresses/Addresses.java
new file mode 100644
index 0000000000..5d8022a9b6
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/addresses/Addresses.java
@@ -0,0 +1,77 @@
+package com.ripple.encodings.addresses;
+
+
+import com.ripple.encodings.base58.B58;
+import com.ripple.encodings.basex.EncodingFormatException;
+import com.ripple.encodings.basex.IBaseX;
+import org.omg.IOP.Encoding;
+
+public class Addresses {
+ public static final B58 codec = new B58("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz");
+
+ // Otherwise known as `family seed`
+ public static final B58.Version SEED_K256 = new B58.Version(
+ 33, "seedK256", 16);
+
+ public static final B58.Version SEED_ED25519 = new B58.Version(
+ "01E14B", "seedEd25519", 16);
+
+ public static final B58.Version ACCOUNT_ID = new B58.Version(
+ 0, "accountId", 20);
+
+ public static final B58.Version NODE_PUBLIC_KEY = new B58.Version(
+ 28, "nodePublicKey", 33);
+
+ public static final B58.Version NODE_PRIVATE_KEY = new B58.Version(
+ 33, "nodePrivateKey", 32);
+
+ public static byte[] decode(String encoded, B58.Version version) {
+ return codec.decodeVersioned(encoded, version).payload;
+ }
+
+ public static String encode(byte[] bytes, B58.Version version) {
+ return codec.encodeVersioned(bytes, version);
+ }
+
+ public static byte[] decodeSeedToBytes(String seed) {
+ return codec.decodeVersioned(seed, SEED_K256, SEED_ED25519)
+ .payload;
+ }
+
+ public static B58.Decoded decodeSeed(String seed) {
+ return codec.decodeVersioned(seed, SEED_K256, SEED_ED25519);
+ }
+
+ public static String encodeSeedK256(byte[] bytes) {
+ return encode(bytes, SEED_K256);
+ }
+
+ public static String encodeAccountID(byte[] bytes) {
+ return encode(bytes, ACCOUNT_ID);
+ }
+
+ public static byte[] decodeAccountID(String id) {
+ return decode(id, ACCOUNT_ID);
+ }
+
+ public static String encodeNodePublic(byte[] bytes) {
+ return encode(bytes, NODE_PUBLIC_KEY);
+ }
+
+ public static byte[] decodeNodePublic(String base58) {
+ return decode(base58, NODE_PUBLIC_KEY);
+ }
+
+ public static boolean isValid(String encoded, B58.Version... versions) {
+ try {
+ codec.decodeVersioned(encoded, versions);
+ return true;
+ } catch (EncodingFormatException e) {
+ return false;
+ }
+ }
+
+ public static boolean isValidAccountID(String encoded) {
+ return isValid(encoded, ACCOUNT_ID);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/encodings/base58/B58.java b/ripple-core/src/main/java/com/ripple/encodings/base58/B58.java
new file mode 100644
index 0000000000..a6b928824e
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/base58/B58.java
@@ -0,0 +1,9 @@
+package com.ripple.encodings.base58;
+
+import com.ripple.encodings.basex.BaseX;
+
+public class B58 extends BaseX {
+ public B58(String alphabet) {
+ super(alphabet);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/encodings/basex/BaseX.java b/ripple-core/src/main/java/com/ripple/encodings/basex/BaseX.java
new file mode 100644
index 0000000000..4ebbfc871a
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/basex/BaseX.java
@@ -0,0 +1,231 @@
+package com.ripple.encodings.basex;
+
+import com.ripple.utils.HashUtils;
+
+import java.util.Arrays;
+
+public class BaseX implements IBaseX {
+ private char[] alphabet;
+ private final char encodedZero;
+ protected final int[] indexes;
+
+ public BaseX(String alphabet_) {
+ if (alphabet_.length() > 256) {
+ throw new IllegalArgumentException();
+ }
+ alphabet = alphabet_.toCharArray();
+ indexes = new int[128];
+ encodedZero = alphabet[0];
+ Arrays.fill(indexes, -1);
+ for (int i = 0; i < alphabet.length; i++) {
+ indexes[alphabet[i]] = i;
+ }
+ }
+
+ private static String repeat(int times, char repeated) {
+ char[] chars = new char[times];
+ Arrays.fill(chars, repeated);
+ return new String(chars);
+ }
+
+ /**
+ * Encodes the given bytes as a base58 string (no checksum is appended).
+ *
+ * @param input the bytes to encode
+ * @return the base58-encoded string
+ */
+ @Override
+ public String encode(byte[] input) {
+ if (input.length == 0) {
+ return "";
+ }
+ // Count leading zeros.
+ int zeros = 0;
+ while (zeros < input.length && input[zeros] == 0) {
+ ++zeros;
+ }
+ // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)
+ input = Arrays.copyOf(input, input.length); // since we modify it in-place
+ char[] encoded = new char[input.length * 2]; // upper bound
+ int outputStart = encoded.length;
+ for (int inputStart = zeros; inputStart < input.length; ) {
+ encoded[--outputStart] = alphabet[divmod(input, inputStart, 256, 58)];
+ if (input[inputStart] == 0) {
+ ++inputStart; // optimization - skip leading zeros
+ }
+ }
+ // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.
+ while (outputStart < encoded.length && encoded[outputStart] == encodedZero) {
+ ++outputStart;
+ }
+ while (--zeros >= 0) {
+ encoded[--outputStart] = encodedZero;
+ }
+ // Return encoded string (including encoded leading zeros).
+ return new String(encoded, outputStart, encoded.length - outputStart);
+ }
+
+ /**
+ * Decodes the given base58 string into the original data bytes.
+ *
+ * @param input the base58-encoded string to decode
+ * @return the decoded data bytes
+ * @throws EncodingFormatException if the given string is not a valid base58 string
+ */
+ @Override
+ public byte[] decode(String input) throws EncodingFormatException {
+ if (input.length() == 0) {
+ return new byte[0];
+ }
+ // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
+ byte[] input58 = new byte[input.length()];
+ for (int i = 0; i < input.length(); ++i) {
+ char c = input.charAt(i);
+ int digit = c < 128 ? indexes[c] : -1;
+ if (digit < 0) {
+ throw new EncodingFormatException("Illegal character " + c + " at position " + i);
+ }
+ input58[i] = (byte) digit;
+ }
+ // Count leading zeros.
+ int zeros = 0;
+ while (zeros < input58.length && input58[zeros] == 0) {
+ ++zeros;
+ }
+ // Convert base-58 digits to base-256 digits.
+ byte[] decoded = new byte[input.length()];
+ int outputStart = decoded.length;
+ for (int inputStart = zeros; inputStart < input58.length; ) {
+ decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
+ if (input58[inputStart] == 0) {
+ ++inputStart; // optimization - skip leading zeros
+ }
+ }
+ // Ignore extra leading zeroes that were added during the calculation.
+ while (outputStart < decoded.length && decoded[outputStart] == 0) {
+ ++outputStart;
+ }
+ // Return decoded data (including original number of leading zeros).
+ return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);
+ }
+
+ /**
+ * Decodes the given base58 string into the original data bytes, using the checksum in the
+ * last 4 bytes of the decoded data to verify that the rest are correct. The checksum is
+ * removed from the returned data.
+ *
+ * @param input the base58-encoded string to decode (which should include the checksum)
+ * @throws EncodingFormatException if the input is not base 58 or the checksum does not validate.
+ */
+ public byte[] decodeChecked(String input) throws EncodingFormatException {
+ byte[] decoded = decode(input);
+ if (decoded.length < 4)
+ throw new EncodingFormatException("Input too short");
+ byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
+ byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
+ byte[] actualChecksum = Arrays.copyOfRange(HashUtils.doubleDigest(data), 0, 4);
+ if (!Arrays.equals(checksum, actualChecksum))
+ throw new EncodingFormatException("Checksum does not validate");
+ return data;
+ }
+
+ /**
+ * Divides a number, represented as an array of bytes each containing a single digit
+ * in the specified base, by the given divisor. The given number is modified in-place
+ * to contain the quotient, and the return value is the remainder.
+ *
+ * @param number the number to divide
+ * @param firstDigit the index within the array of the first non-zero digit
+ * (this is used for optimization by skipping the leading zeros)
+ * @param base the base in which the number's digits are represented (up to 256)
+ * @param divisor the number to divide by (up to 256)
+ * @return the remainder of the division operation
+ */
+ private byte divmod(byte[] number, int firstDigit, int base, int divisor) {
+ // this is just long division which accounts for the base of the input digits
+ int remainder = 0;
+ for (int i = firstDigit; i < number.length; i++) {
+ int digit = (int) number[i] & 0xFF;
+ int temp = remainder * base + digit;
+ number[i] = (byte) (temp / divisor);
+ remainder = temp % divisor;
+ }
+ return (byte) remainder;
+ }
+
+ @Override
+ public byte[] findPrefix(int payLoadLength, String desiredPrefix) {
+ if (alphabet.length != 58) {
+ throw new IllegalStateException("Must be base58");
+ }
+ int totalLength = payLoadLength + 4; // for the checksum
+ double chars = Math.log(Math.pow(256, totalLength)) / Math.log(58);
+ int requiredChars = (int) Math.ceil(chars + 0.2D);
+ // Mess with this to see stability tests fail
+ int charPos = (alphabet.length / 2) - 1;
+ char padding = alphabet[(charPos)];
+ String template = desiredPrefix + repeat(requiredChars, padding);
+ byte[] decoded = decode(template);
+ return copyOfRange(decoded, 0, decoded.length - totalLength);
+ }
+
+ @Override
+ public String encodeVersioned(byte[] input, Version version) {
+ if (input.length != version.expectedLength) {
+ throw new IllegalArgumentException(
+ "input length=" + input.length +
+ ", expected=" + version.expectedLength);
+ }
+ return encode(concatVersionAndAddChecksum(input, version.bytes));
+ }
+
+ private byte[] concatVersionAndAddChecksum(byte[] input, byte[] version) {
+ byte[] buffer = new byte[input.length + version.length];
+ System.arraycopy(version, 0, buffer, 0, version.length);
+ System.arraycopy(input, 0, buffer, version.length, input.length);
+ byte[] checkSum = copyOfRange(HashUtils.doubleDigest(buffer), 0, 4);
+ byte[] output = new byte[buffer.length + checkSum.length];
+ System.arraycopy(buffer, 0, output, 0, buffer.length);
+ System.arraycopy(checkSum, 0, output, buffer.length, checkSum.length);
+ return output;
+ }
+
+ public Decoded decodeVersioned(String input,
+ Version... possibleVersions) throws EncodingFormatException {
+
+ byte[] buffer = decodeChecked(input);
+
+ Version foundVersion = null;
+ int expectedLength = possibleVersions[0].expectedLength;
+ int versionLength = buffer.length - expectedLength;
+ byte[] versionBytes = copyOfRange(buffer, 0, versionLength);
+
+ for (Version possible : possibleVersions) {
+ if (possible.expectedLength != expectedLength) {
+ throw new IllegalStateException();
+ }
+
+ if (Arrays.equals(possible.bytes, versionBytes)) {
+ foundVersion = possible;
+ break;
+ }
+ }
+ if (foundVersion == null) {
+ throw new EncodingFormatException("Incorrect version");
+ }
+
+ byte[] bytes = copyOfRange(buffer, versionLength, buffer.length);
+ if (bytes.length != expectedLength) {
+ throw new EncodingFormatException("Incorrect length");
+ }
+
+ return new Decoded(foundVersion, bytes);
+ }
+
+ private byte[] copyOfRange(byte[] source, int from, int to) {
+ byte[] range = new byte[to - from];
+ System.arraycopy(source, from, range, 0, range.length);
+ return range;
+ }
+
+}
\ No newline at end of file
diff --git a/ripple-core/src/main/java/com/ripple/encodings/basex/EncodingFormatException.java b/ripple-core/src/main/java/com/ripple/encodings/basex/EncodingFormatException.java
new file mode 100644
index 0000000000..4c56ffa1e9
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/basex/EncodingFormatException.java
@@ -0,0 +1,7 @@
+package com.ripple.encodings.basex;
+
+public class EncodingFormatException extends RuntimeException{
+ public EncodingFormatException(String message) {
+ super(message);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/encodings/basex/IBaseX.java b/ripple-core/src/main/java/com/ripple/encodings/basex/IBaseX.java
new file mode 100644
index 0000000000..84d98aa51b
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/basex/IBaseX.java
@@ -0,0 +1,41 @@
+package com.ripple.encodings.basex;
+
+import com.ripple.encodings.common.B16;
+
+public interface IBaseX {
+ String encode(byte[] input);
+ byte[] decode(String input);
+
+ byte[] findPrefix(int payLoadLength, String desiredPrefix);
+
+ String encodeVersioned(byte[] input, Version version);
+ Decoded decodeVersioned(String input, Version... possibleVersions);
+
+ class Decoded {
+ public final Version version;
+ public final byte[] payload;
+
+ Decoded(Version version, byte[] payload) {
+ this.version = version;
+ this.payload = payload;
+ }
+ }
+
+ class Version {
+ public final byte[] bytes;
+ public final String name;
+ public final int expectedLength;
+
+ public Version(byte[] bytes, String name, int length) {
+ this.bytes = bytes;
+ this.name = name;
+ this.expectedLength = length;
+ }
+ public Version(int aByte, String name, int length) {
+ this(new byte[]{(byte) aByte}, name, length);
+ }
+ public Version(String hex, String name, int length) {
+ this(B16.decode(hex), name, length);
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/encodings/common/B16.java b/ripple-core/src/main/java/com/ripple/encodings/common/B16.java
new file mode 100644
index 0000000000..9ab895721c
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/common/B16.java
@@ -0,0 +1,20 @@
+package com.ripple.encodings.common;
+
+
+import static org.bouncycastle.util.encoders.Hex.toHexString;
+
+public class B16 {
+ public static String toStringTrimmed(byte[] bytes) {
+ int offset = 0;
+ if (bytes[0] == 0) {
+ offset = 1;
+ }
+ return toHexString(bytes, offset, bytes.length - offset).toUpperCase();
+ }
+ public static String encode(byte[] bytes) {
+ return toHexString(bytes).toUpperCase();
+ }
+ public static byte[] decode(String hex) {
+ return org.bouncycastle.util.encoders.Hex.decode(hex);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/encodings/common/B64.java b/ripple-core/src/main/java/com/ripple/encodings/common/B64.java
new file mode 100644
index 0000000000..e48b7d1a70
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/encodings/common/B64.java
@@ -0,0 +1,12 @@
+package com.ripple.encodings.common;
+
+import org.bouncycastle.util.encoders.Base64;
+
+public class B64 {
+ public static String toString(byte[] bytes) {
+ return Base64.toBase64String(bytes);
+ }
+ public static byte[] decode(String string) {
+ return Base64.decode(string);
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/utils/HashUtils.java b/ripple-core/src/main/java/com/ripple/utils/HashUtils.java
new file mode 100644
index 0000000000..2d79986957
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/utils/HashUtils.java
@@ -0,0 +1,62 @@
+package com.ripple.utils;
+
+import org.bouncycastle.crypto.digests.RIPEMD160Digest;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class HashUtils {
+ private static final MessageDigest digest;
+ static {
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e); // Can't happen.
+ }
+ }
+
+ /**
+ * See {@link HashUtils#doubleDigest(byte[], int, int)}.
+ */
+ public static byte[] doubleDigest(byte[] input) {
+ return doubleDigest(input, 0, input.length);
+ }
+
+ /**
+ * Calculates the SHA-256 hash of the given byte range, and then hashes the resulting hash again. This is
+ * standard procedure in Bitcoin. The resulting hash is in big endian form.
+ */
+ public static byte[] doubleDigest(byte[] input, int offset, int length) {
+ synchronized (digest) {
+ digest.reset();
+ digest.update(input, offset, length);
+ byte[] first = digest.digest();
+ return digest.digest(first);
+ }
+ }
+
+ public static byte[] halfSha512(byte[] bytes) {
+ return new Sha512(bytes).finish256();
+ }
+
+ public static byte[] quarterSha512(byte[] bytes) {
+ return new Sha512(bytes).finish128();
+ }
+
+ public static byte[] sha512(byte[] bytes) {
+ return new Sha512(bytes).finish();
+ }
+
+ public static byte[] SHA256_RIPEMD160(byte[] input) {
+ try {
+ byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(input);
+ RIPEMD160Digest digest = new RIPEMD160Digest();
+ digest.update(sha256, 0, sha256.length);
+ byte[] out = new byte[20];
+ digest.doFinal(out, 0);
+ return out;
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e); // Cannot happen.
+ }
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/utils/Sha512.java b/ripple-core/src/main/java/com/ripple/utils/Sha512.java
new file mode 100644
index 0000000000..bafd20b134
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/utils/Sha512.java
@@ -0,0 +1,53 @@
+package com.ripple.utils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+public class Sha512 {
+ private MessageDigest messageDigest;
+
+ public Sha512() {
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-512");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Sha512(byte[] start) {
+ this();
+ add(start);
+ }
+
+ public Sha512 add(byte[] bytes) {
+ messageDigest.update(bytes);
+ return this;
+ }
+
+ public Sha512 addU32(int i) {
+ messageDigest.update((byte) ((i >>> 24) & 0xFF));
+ messageDigest.update((byte) ((i >>> 16) & 0xFF));
+ messageDigest.update((byte) ((i >>> 8) & 0xFF));
+ messageDigest.update((byte) ((i) & 0xFF));
+ return this;
+ }
+
+ private byte[] finishTaking(int size) {
+ byte[] hash = new byte[size];
+ System.arraycopy(messageDigest.digest(), 0, hash, 0, size);
+ return hash;
+ }
+
+ public byte[] finish128() {
+ return finishTaking(16);
+ }
+
+ public byte[] finish256() {
+ return finishTaking(32);
+ }
+
+ public byte[] finish() {
+ return messageDigest.digest();
+ }
+}
diff --git a/ripple-core/src/main/java/com/ripple/utils/Utils.java b/ripple-core/src/main/java/com/ripple/utils/Utils.java
new file mode 100644
index 0000000000..8376f1637f
--- /dev/null
+++ b/ripple-core/src/main/java/com/ripple/utils/Utils.java
@@ -0,0 +1,58 @@
+package com.ripple.utils;
+
+import com.ripple.encodings.common.B16;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+
+public class Utils {
+ public static String bigHex(BigInteger bn) {
+ return B16.toStringTrimmed(bn.toByteArray());
+ }
+
+ public static BigInteger uBigInt(byte[] bytes) {
+ return new BigInteger(1, bytes);
+ }
+
+ public static byte[] leadingZeroesTrimmedOrPaddedTo(int size, byte[] bytes) {
+ if (bytes.length == size) {
+ return bytes;
+ }
+ if (bytes.length > size) {
+ int length = bytes.length;
+ int offset = 0;
+ while (length > size && bytes[offset++] == 0) {
+ length--;
+ }
+ if (length > size) {
+ throw new IllegalArgumentException(
+ "bytes.length: " + bytes.length + " > " +
+ size);
+ }
+ return copyFrom(bytes, offset, size);
+ }
+
+ return padTo(bytes, size);
+ }
+
+ public static byte[] padTo256(byte[] bytes) {
+ return leadingZeroesTrimmedOrPaddedTo(32, bytes);
+ }
+
+ public static byte[] padTo160(byte[] bytes) {
+ return leadingZeroesTrimmedOrPaddedTo(20, bytes);
+ }
+
+ private static byte[] copyFrom(byte[] bytes, int offset, int size) {
+ byte[] result = new byte[size];
+ System.arraycopy(bytes, offset, result, 0, size);
+ return result;
+ }
+
+ private static byte[] padTo(byte[] bytes, int size) {
+ byte[] result = new byte[size];
+ int offset = result.length - bytes.length;
+ System.arraycopy(bytes, 0, result, offset, bytes.length);
+ return result;
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index ac821b1c6c..7c7add5bef 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':card-android', ':server-android', ':card-common'
+include ':app', ':card-android', ':server-android', ':card-common', ':ripple-core'