From 102131b70cd35dbbadb95e6aef274e7eda67c0b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Feb 2019 19:26:53 +0300 Subject: [PATCH 01/11] Updated on 2026-08-14 --- app/build.gradle | 2 + .../main/java/com/tangem/data/Blockchain.java | 3 +- .../com/tangem/data/network/AdaliteApi.java | 29 + .../tangem/data/network/ServerApiAdalite.java | 123 + .../data/network/model/AdaliteResponse.kt | 37 + .../com/tangem/domain/wallet/CoinEngine.java | 4 +- .../tangem/domain/wallet/CoinEngineFactory.kt | 4 + .../tangem/domain/wallet/btc/BtcEngine.java | 1 - .../tangem/domain/wallet/cardano/Blake2b.java | 2098 +++++++++++++++++ .../domain/wallet/cardano/CardanoData.java | 122 + .../domain/wallet/cardano/CardanoEngine.java | 665 ++++++ .../domain/wallet/cardano/CardanoUtils.java | 4 + .../domain/wallet/ducatus/DucatusEngine.java | 2 +- .../presentation/fragment/LoadedWallet.kt | 1 + 14 files changed, 3091 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/tangem/data/network/AdaliteApi.java create mode 100644 app/src/main/java/com/tangem/data/network/ServerApiAdalite.java create mode 100644 app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt create mode 100644 app/src/main/java/com/tangem/domain/wallet/cardano/Blake2b.java create mode 100644 app/src/main/java/com/tangem/domain/wallet/cardano/CardanoData.java create mode 100644 app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java create mode 100644 app/src/main/java/com/tangem/domain/wallet/cardano/CardanoUtils.java diff --git a/app/build.gradle b/app/build.gradle index 145de07bc0..69451846c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,6 +51,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' implementation 'androidx.biometric:biometric:1.0.0-alpha03' + implementation 'co.nstant.in:cbor:0.8' implementation 'com.google.android.material:material:1.1.0-alpha02' implementation 'com.google.dagger:dagger:2.21' kapt 'com.google.dagger:dagger-compiler:2.21' @@ -62,6 +63,7 @@ dependencies { implementation 'com.scottyab:rootbeer-lib:0.0.7' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.squareup.retrofit2:retrofit:2.5.0' + implementation 'com.squareup.retrofit2:converter-scalars:2.1.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' implementation 'com.skyfishjy.ripplebackground:library:1.0.1' implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 9e95661c57..11a9ec45dd 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -16,7 +16,8 @@ public enum Blockchain { 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"); + RootstockToken("Token", "RBTC", 1.0, R.drawable.ic_logo_bat_token, "Rootstock"), + Cardano("CARDANO", "ADA", 1000000.0,R.drawable.ic_logo_bitcoin, "Cardano"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/AdaliteApi.java b/app/src/main/java/com/tangem/data/network/AdaliteApi.java new file mode 100644 index 0000000000..a081ba8862 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/AdaliteApi.java @@ -0,0 +1,29 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.AdaliteResponse; +import com.tangem.data.network.model.AdaliteResponseUtxo; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface AdaliteApi { + @GET(ServerApiAdalite.ADALITE_ADDRESS) + Call adaliteAddress(@Path("address") String address); + + @Headers("Content-Type: application/json") + @POST(ServerApiAdalite.ADALITE_UNSPENT_OUTPUTS) + Call adaliteUnspent(@Body String address); + +// @GET(ServerApiAdalite.ADALITE_TRANSACTION) +// Call adaliteTransaction(@Path("txId") String txId); + + @Headers("Content-Type: application/json") + @POST(ServerApiAdalite.ADALITE_SEND) + Call adaliteSend(@Body String rawTx ); +} diff --git a/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java b/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java new file mode 100644 index 0000000000..f5192f2034 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java @@ -0,0 +1,123 @@ +package com.tangem.data.network; + +import android.util.Log; + +import com.tangem.data.network.model.AdaliteResponse; +import com.tangem.data.network.model.AdaliteResponseUtxo; + +import androidx.annotation.NonNull; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; + +public class ServerApiAdalite { + private static String TAG = ServerApiAdalite.class.getSimpleName(); + + public static final String ADALITE_ADDRESS = "/api/addresses/summary/{address}"; + public static final String ADALITE_UNSPENT_OUTPUTS = "/api/bulk/addresses/utxo"; + // public static final String ADALITE_TRANSACTION = "/api/txs/raw/{txId}}"; + public static final String ADALITE_SEND = "/tx/send"; + + 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, AdaliteResponse adaliteResponse); + + void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo); + + void onFail(String method, String message); + } + + public void setResponseListener(ResponseListener listener) { + responseListener = listener; + } + + public void requestData(String method, String wallet, String tx) { + requestsCount++; + String adaliteURL = "https://explorer.adalite.io"; //TODO: make random selection + this.lastNode = adaliteURL; //TODO: show node instead of URL + + Retrofit retrofitAdalite = new Retrofit.Builder() + .baseUrl(adaliteURL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + AdaliteApi adaliteApi = retrofitAdalite.create(AdaliteApi.class); + + if (method.equals(ADALITE_UNSPENT_OUTPUTS)) { + Call call = adaliteApi.adaliteUnspent("[\"" + wallet + "\"]"); + 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()); + } + }); + } else { + Call call; + switch (method) { + case ADALITE_ADDRESS: + call = adaliteApi.adaliteAddress(wallet); + break; + +// case ADALITE_TRANSACTION: +// call = adaliteApi.adaliteTransaction(tx); +// break; + + case ADALITE_SEND: + call = adaliteApi.adaliteSend(tx); + break; + + default: + call = adaliteApi.adaliteAddress(wallet); + break; + } + + 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/AdaliteResponse.kt b/app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt new file mode 100644 index 0000000000..f0a3a1ca44 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt @@ -0,0 +1,37 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class AdaliteResponse( + @SerializedName("Right") + var right: AddressData? = null +) + +data class AdaliteResponseUtxo( + @SerializedName("Right") + var right: List +) + +data class AddressData( + @SerializedName("caAddress") + var caAddress: String? = null, + + @SerializedName("caBalance") + var caBalance: AdaliteCoins? = null +) + +data class AdaliteCoins( + @SerializedName("getCoin") + var getCoin: Long? = null +) + +data class UtxoData( + @SerializedName("cuId") + var cuId: String? = null, + + @SerializedName("cuOutIndex") + var cuOutIndex: Int? = null, + + @SerializedName("cuCoins") + var cuCoins: AdaliteCoins? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java index a5b39c5ed9..702dcb8097 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java @@ -15,6 +15,8 @@ import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; +import co.nstant.in.cbor.CborException; + /** * Created by Ilia on 15.02.2018. */ @@ -214,7 +216,7 @@ public abstract class CoinEngine { public abstract boolean validateAddress(String address); - public abstract String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException; + public abstract String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException, CborException; public abstract Amount convertToAmount(InternalAmount internalAmount) throws Exception; diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt index 9d99f0121f..00e26bc4cb 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt @@ -7,6 +7,7 @@ import com.tangem.domain.wallet.eth.EthEngine import com.tangem.domain.wallet.token.TokenEngine import com.tangem.domain.wallet.bch.BtcCashEngine import com.tangem.data.Blockchain +import com.tangem.domain.wallet.cardano.CardanoEngine import com.tangem.domain.wallet.ltc.LtcEngine import com.tangem.domain.wallet.nftToken.NftTokenEngine import com.tangem.domain.wallet.rsk.RskEngine @@ -33,6 +34,7 @@ object CoinEngineFactory { Blockchain.Litecoin -> LtcEngine() Blockchain.Rootstock -> RskEngine() Blockchain.RootstockToken -> RskTokenEngine() + Blockchain.Cardano -> CardanoEngine() else -> null } } @@ -57,6 +59,8 @@ object CoinEngineFactory { RskEngine(context) else if (Blockchain.RootstockToken == context.blockchain) RskTokenEngine(context) + else if (Blockchain.Cardano == context.blockchain) + CardanoEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java index 770df91894..664b1623fa 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java @@ -195,7 +195,6 @@ public class BtcEngine extends CoinEngine { @Override public Uri getShareWalletUri() { - byte[] x = new byte[] {0,0,0,0,0,0,0,0}; if (ctx.getCard().getDenomination() != null && !ctx.getCard().getDenominationText().equals("0.00")) { return Uri.parse("bitcoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8)); } else { diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/Blake2b.java b/app/src/main/java/com/tangem/domain/wallet/cardano/Blake2b.java new file mode 100644 index 0000000000..b50c045435 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/Blake2b.java @@ -0,0 +1,2098 @@ +/* !!! Doost !!! */ + +/* + A Java implementation of BLAKE2B cryptographic digest algorithm. + + Joubin Mohammad Houshyar + bushwick, nyc + 02-14-2014 + + -- + + To the extent possible under law, the author(s) have dedicated all copyright + and related and neighboring rights to this software to the public domain + worldwide. This software is distributed without any warranty. + + You should have received a copy of the CC0 Public Domain Dedication along with + this software. If not, see . +*/ + +package com.tangem.domain.wallet.cardano; + +import java.io.PrintStream; +import java.io.Serializable; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; // JCE not supported / anticipated .. +import java.util.Arrays; + +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.Assert.assertFail; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.Assert.exclusiveLowerBound; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.Assert.inclusiveLowerBound; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.Assert.inclusiveUpperBound; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.LittleEndian.readInt; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.LittleEndian.readLong; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.LittleEndian.writeInt; +import static com.tangem.domain.wallet.cardano.Blake2b.Engine.LittleEndian.writeLong; + + + +/** */ +public interface Blake2b { + // --------------------------------------------------------------------- + // Specification + // --------------------------------------------------------------------- + public interface Spec { + /** pblock size of blake2b */ + int param_bytes = 64; + + /** pblock size of blake2b */ + int block_bytes = 128; + + /** maximum digest size */ + int max_digest_bytes = 64; + + /** maximum key sie */ + int max_key_bytes = 64; + + /** maximum salt size */ + int max_salt_bytes = 16; + + /** maximum personalization string size */ + int max_personalization_bytes = 16; + + /** length of h space vector array */ + int state_space_len = 8; + + /** max tree fanout value */ + int max_tree_fantout = 0xFF; + + /** max tree depth value */ + int max_tree_depth = 0xFF; + + /** max tree leaf length value.Note that this has uint32 semantics + and thus 0xFFFFFFFF is used as max value limit. */ + int max_tree_leaf_length = 0xFFFFFFFF; + + /** max node offset value. Note that this has uint64 semantics + and thus 0xFFFFFFFFFFFFFFFFL is used as max value limit. */ + long max_node_offset = 0xFFFFFFFFFFFFFFFFL; + + /** max tree inner length value */ + int max_tree_inner_length = 0xFF; + + /** initialization values map ref-Spec IV[i] -> slice iv[i*8:i*8+7] */ + long[] IV = { + 0x6a09e667f3bcc908L, + 0xbb67ae8584caa73bL, + 0x3c6ef372fe94f82bL, + 0xa54ff53a5f1d36f1L, + 0x510e527fade682d1L, + 0x9b05688c2b3e6c1fL, + 0x1f83d9abfb41bd6bL, + 0x5be0cd19137e2179L + }; + } + + // --------------------------------------------------------------------- + // API + // --------------------------------------------------------------------- + // TODO add ByteBuffer variants + + /** + * A serializable / JSON-izable object usable for pausing a hash-in-process + * which can then be resumed with the same Parameter the original digest + * was constructed with, and fed additional bytes to conclude the hash. + */ + public static final class ResumeHandle implements Serializable { + /** per spec */ + public long[] h = new long [ 8 ]; + /** per spec */ + public long[] t = new long [ 2 ]; + /** per spec */ + public long[] f = new long [ 2 ]; + /** per spec (tree) */ + public boolean last_node = false; + /** pulled up 2b optimal */ + public long[] m = new long [16]; + /** pulled up 2b optimal */ + public long[] v = new long [16]; + + /** compressor cache buffer */ + public byte[] buffer; + /** compressor cache buffer offset/cached data length */ + public int buflen; + + /** digest length from init param - copied here on init */ + public int outlen; + + public int type; + + /** + * Create a reconstituted Black2b digest from + * + * @param param + * @return + */ + public Blake2b resume(Param param) { + assert this.buffer != null && this.buffer.length == Spec.block_bytes; + assert this.h != null && this.h.length == 8 + && this.t != null && this.t.length == 2 + && this.f != null && this.f.length == 2 + && this.m != null && this.m.length == 16 + && this.v != null && this.v.length == 16 : "Data is corrupted"; + assert this.outlen == param.getDigestLength() : "Not originally initialized from this param"; + assert this.type == 1 || this.type == 2 : "Unknown type " + this.type; + Engine.State state = new Engine.State(outlen, this.type == 1); + System.arraycopy(this.h, 0, state.h, 0, state.h.length); + System.arraycopy(this.t, 0, state.t, 0, state.t.length); + System.arraycopy(this.f, 0, state.f, 0, state.f.length); + System.arraycopy(this.m, 0, m, 0, m.length); + System.arraycopy(this.v, 0, v, 0, v.length); + System.arraycopy(this.buffer, 0, state.buffer, 0, state.buffer.length); + state.buflen = buflen; + return type == 1 ? new Mac(param, state) : new Digest(param, state); + } + } + + /** */ + void update(byte[] input) ; + + /** */ + void update(byte input) ; + + /** */ + void update(byte[] input, int offset, int len) ; + + /** */ + byte[] digest() ; + + /** */ + byte[] digest(byte[] input) ; + + /** */ + void digest(byte[] output, int offset, int len) ; + + /** */ + void reset() ; + + ResumeHandle state(); + // --------------------------------------------------------------------- + // Blake2b Message Digest + // --------------------------------------------------------------------- + + /** Generalized Blake2b digest. */ + public static class Digest extends Engine implements Blake2b { + private Digest (final Param p) { super (p); } + private Digest () { super (); } + private Digest(Param p, State state) { + super(state, p); + } + + public static Digest newInstance () { + return new Digest (); + } + public static Digest newInstance (final int digestLength) { + return new Digest (new Param().setDigestLength(digestLength)); + } + public static Digest newInstance (Param p) { + return new Digest (p); + } + public static Digest newInstance (Param p, State state) { + return new Digest(p, state); + } + } + + // --------------------------------------------------------------------- + // Blake2b Message Authentication Code + // --------------------------------------------------------------------- + + /** Message Authentication Code (MAC) digest. */ + public static class Mac extends Engine implements Blake2b { + private Mac (final Param p, State state) { super (state, p); } + private Mac (final Param p) { super (p); } + private Mac () { super (); } + + /** Blake2b.MAC 512 - using default Blake2b.Spec settings with given key */ + public static Mac newInstance (final byte[] key) { + return new Mac (new Param().setKey(key)); + } + /** Blake2b.MAC - using default Blake2b.Spec settings with given key, with given digest length */ + public static Mac newInstance (final byte[] key, final int digestLength) { + return new Mac (new Param().setKey(key).setDigestLength(digestLength)); + } + /** Blake2b.MAC - using default Blake2b.Spec settings with given java.security.Key, with given digest length */ + public static Mac newInstance (final Key key, final int digestLength) { + return new Mac (new Param().setKey(key).setDigestLength(digestLength)); + } + /** Blake2b.MAC - using the specified Parameters. + * @param p asserted valid configured Param with key */ + public static Mac newInstance (Param p) { + assert p != null : "Param (p) is null"; + assert p.hasKey() : "Param (p) not configured with a key"; + return new Mac (p); + } + } + + // --------------------------------------------------------------------- + // Blake2b Incremental Message Digest (Tree) + // --------------------------------------------------------------------- + + /** + * Note that Tree is just a convenience class; incremental hash (tree) + * can be done directly with the Digest class. + *
+ * Further node, that tree does NOT accumulate the leaf hashes -- + * you need to do that + */ + public static class Tree { + + final int depth; + final int fanout; + final int leaf_length; + final int inner_length; + final int digest_length; + + /** + * + * @param fanout + * @param depth + * @param leaf_length size of data input for leaf nodes. + * @param inner_length note this is used also as digest-length for non-root nodes. + * @param digest_length final hash out digest-length for the tree + */ + public Tree ( + final int depth, + final int fanout, + final int leaf_length, + final int inner_length, + final int digest_length + ) { + this.fanout = fanout; + this.depth = depth; + this.leaf_length = leaf_length; + this.inner_length = inner_length; + this.digest_length = digest_length; + } + private Param treeParam() { + return new Param(). + setDepth(depth).setFanout(fanout).setLeafLength(leaf_length).setInnerLength(inner_length); + } + /** returns the Digest for tree node @ (depth, offset) */ + public final Digest getNode (final int depth, final int offset) { + final Param nodeParam = treeParam().setNodeDepth(depth).setNodeOffset(offset).setDigestLength(inner_length); + return Digest.newInstance(nodeParam); + } + /** returns the Digest for root node */ + public final Digest getRoot () { + final int depth = this.depth - 1; + final Param rootParam = treeParam().setNodeDepth(depth).setNodeOffset(0L).setDigestLength(digest_length); + return Digest.newInstance(rootParam); + } + } + + // --------------------------------------------------------------------- + // Engine + // --------------------------------------------------------------------- + static class Engine implements Blake2b { + + // --------------------------------------------------------------------- + // Blake2b State(+) per reference implementation + // --------------------------------------------------------------------- + // REVU: address last_node TODO part of the Tree/incremental + static final class State { + /** per spec */ + private final long[] h = new long [ 8 ]; + /** per spec */ + private final long[] t = new long [ 2 ]; + /** per spec */ + private final long[] f = new long [ 2 ]; + /** per spec (tree) */ + private boolean last_node = false; + /** pulled up 2b optimal */ + private final long[] m = new long [16]; + /** pulled up 2b optimal */ + private final long[] v = new long [16]; + + /** compressor cache buffer */ + private final byte[] buffer; + /** compressor cache buffer offset/cached data length */ + private int buflen; + + /** digest length from init param - copied here on init */ + private final int outlen; + + private final int digestType; + + State(int digestLength, boolean isMac) { + this.buffer = new byte [ Spec.block_bytes ]; + this.outlen = digestLength; + // do not use zero, so we can detect serialization errors + this.digestType = isMac ? 1 : 2; + } + + public ResumeHandle toResumableForm() { + ResumeHandle state = new ResumeHandle(); + state.h = h; + state.t = t; + state.f = f; + state.last_node = last_node; + state.m = m; + state.v = v; + state.buffer = buffer; + state.buflen = buflen; + state.outlen = outlen; + state.type = digestType; + return state; + } + } + + private State state; + /** configuration params */ + private final Param param; + + /** read only */ + private static byte[] zeropad = new byte [ Spec.block_bytes ]; + + /** a little bit of semantics */ + interface flag { + int last_block = 0; + int last_node = 1; + } + /** to support update(byte) */ + private final byte[] oneByte = new byte[1]; + + // --------------------------------------------------------------------- + // Ctor & Initialization + // --------------------------------------------------------------------- + + /** Basic use constructor pending (TODO) JCA/JCE compliance */ + Engine () { + this( new Param() ); + } + + Engine(State state, Param param) { + assert state != null : "state is null"; + assert param != null : "param is null"; + this.state = state; + this.param = param; + } + + /** User provided Param for custom configurations */ + Engine (final Param param) { + assert param != null : "param is null"; + this.param = param; + state = new State(param.getDigestLength(), this instanceof Mac); + if ( param.getDepth() > Param.Default.depth ) { + final int ndepth = param.getNodeDepth(); + final long nxoff = param.getNodeOffset(); + if (ndepth == param.getDepth() - 1) { + state.last_node = true; + assert nxoff == 0 : "root must have offset of zero"; + } else if ( nxoff == param.getFanout() - 1) { + this.state.last_node = true; + } + } + + initialize(); + } + + public ResumeHandle state() { + return state.toResumableForm(); + } + + private void initialize () { + // state vector h - copy values to address reset() requests + System.arraycopy( param.initialized_H(), 0, this.state.h, 0, Spec.state_space_len); + + // if we have a key update initial block + // Note param has zero padded key_bytes to Spec.max_key_bytes + if(param.hasKey){ + this.update (param.key_bytes, 0, Spec.block_bytes); + } + } + + public static void main(String... args) { + Blake2b mac = Mac.newInstance("LOVE".getBytes()); + final byte[] hash = mac.digest("Salaam!".getBytes()); +// Debug.dumpBuffer(System.out, "-- mac hash --", hash); + } + + // --------------------------------------------------------------------- + // interface: Blake2b API + // --------------------------------------------------------------------- + + /** {@inheritDoc} */ + @Override final public void reset () { + // reset cache + this.state.buflen = 0; + for(int i=0; i < state.buffer.length; i++){ + state.buffer[ i ] = (byte) 0; + } + + // reset flags + this.state.f[ 0 ] = 0L; + this.state.f[ 1 ] = 0L; + + // reset counters + this.state.t[ 0 ] = 0L; + this.state.t[ 1 ] = 0L; + + // reset state vector + // NOTE: keep as last stmt as init calls update0 for MACs. + initialize(); + } + + /** {@inheritDoc} */ + @Override final public void update (final byte[] b, int off, int len) { + if (b == null) { + throw new IllegalArgumentException("input buffer (b) is null"); + } + /* zero or more calls to compress */ + final long[] t = state.t; + final byte[] buffer = state.buffer; + while (len > 0) { + if ( state.buflen == 0) { + /* try compressing direct from input ? */ + while ( len > Spec.block_bytes ) { + t[0] += Spec.block_bytes; + t[1] += (t[0] < 0 && state.buflen > -t[0]) ? 1 : 0; + compress( b, off); + len -= Spec.block_bytes; + off += Spec.block_bytes; + } + } else if ( state.buflen == Spec.block_bytes ) { + /* flush */ + t[0] += Spec.block_bytes; + t[1] += t[0] == 0 ? 1 : 0; + compress( buffer, 0 ); + state.buflen = 0; + continue; + } + + // "are we there yet?" + if( len == 0 ) return; + + final int cap = Spec.block_bytes - state.buflen; + final int fill = len > cap ? cap : len; + System.arraycopy( b, off, buffer, state.buflen, fill ); + state.buflen += fill; + len -= fill; + off += fill; + } + } + + /** {@inheritDoc} */ + @Override final public void update (byte b) { + oneByte[0] = b; + update (oneByte, 0, 1); + } + + /** {@inheritDoc} */ + @Override final public void update(byte[] input) { + update (input, 0, input.length); + } + + /** {@inheritDoc} */ + @Override final public void digest(byte[] output, int off, int len) { + // zero pad last block; set last block flags; and compress + System.arraycopy( zeropad, 0, state.buffer, state.buflen, Spec.block_bytes - state.buflen); + if(state.buflen > 0) { + this.state.t[0] += state.buflen; + this.state.t[1] += this.state.t[0] == 0 ? 1 : 0; + } + + this.state.f[ flag.last_block ] = 0xFFFFFFFFFFFFFFFFL; + this.state.f[ flag.last_node ] = this.state.last_node ? 0xFFFFFFFFFFFFFFFFL : 0x0L; + + // compres and write final out (truncated to len) to output + compress( state.buffer, 0 ); + hashout( output, off, len ); + + reset(); + } + + /** {@inheritDoc} */ + @Override final public byte[] digest () throws IllegalArgumentException { + final byte[] out = new byte [state.outlen]; + digest ( out, 0, state.outlen ); + return out; + } + + /** {@inheritDoc} */ + @Override final public byte[] digest (byte[] input) { + update(input, 0, input.length); + return digest(); + } + + // --------------------------------------------------------------------- + // Internal Ops + // --------------------------------------------------------------------- + + /** + * write out the digest output from the 'h' registers. + * truncate full output if necessary. + */ + private void hashout (final byte[] out, final int offset, final int hashlen) { + // write max number of whole longs + final int lcnt = hashlen >>> 3; + long v = 0; + int i = offset; + final long[] h = state.h; + for (int w = 0; w < lcnt; w++) { + v = h [ w ]; + out [ i ] = (byte) v; v >>>= 8; + out [ i+1 ] = (byte) v; v >>>= 8; + out [ i+2 ] = (byte) v; v >>>= 8; + out [ i+3 ] = (byte) v; v >>>= 8; + out [ i+4 ] = (byte) v; v >>>= 8; + out [ i+5 ] = (byte) v; v >>>= 8; + out [ i+6 ] = (byte) v; v >>>= 8; + out [ i+7 ] = (byte) v; + i+=8; + } + + // basta? + if( hashlen == Spec.max_digest_bytes) return; + + // write the remaining bytes of a partial long value + v = h [ lcnt ]; + i = lcnt << 3; + while( i < hashlen ) { + out [ offset + i ] = (byte) v; v >>>= 8; ++i; + } + } + + //////////////////////////////////////////////////////////////////////// + /// Compression Kernel /////////////////////////////////////////// BEGIN + //////////////////////////////////////////////////////////////////////// + + /** compress Spec.block_bytes data from b, from offset */ + private void compress (final byte[] b, final int offset) { + + // set m registers + final long[] m = state.m; + m[ 0] = LittleEndian.readLong(b, offset); + m[ 1] = LittleEndian.readLong(b, offset + 8); + m[ 2] = LittleEndian.readLong(b, offset + 16); + m[ 3] = LittleEndian.readLong(b, offset + 24); + m[ 4] = LittleEndian.readLong(b, offset + 32); + m[ 5] = LittleEndian.readLong(b, offset + 40); + m[ 6] = LittleEndian.readLong(b, offset + 48); + m[ 7] = LittleEndian.readLong(b, offset + 56); + m[ 8] = LittleEndian.readLong(b, offset + 64); + m[ 9] = LittleEndian.readLong(b, offset + 72); + m[10] = LittleEndian.readLong(b, offset + 80); + m[11] = LittleEndian.readLong(b, offset + 88); + m[12] = LittleEndian.readLong(b, offset + 96); + m[13] = LittleEndian.readLong(b, offset + 104); + m[14] = LittleEndian.readLong(b, offset + 112); + m[15] = LittleEndian.readLong(b, offset + 120); + + // set v registers + final long[] v = state.v; + final long[] h = state.h; + final long[] t = state.t; + final long[] f = state.f; + v[ 0] = h[0]; + v[ 1] = h[1]; + v[ 2] = h[2]; + v[ 3] = h[3]; + v[ 4] = h[4]; + v[ 5] = h[5]; + v[ 6] = h[6]; + v[ 7] = h[7]; + v[ 8] = 0x6a09e667f3bcc908L; + v[ 9] = 0xbb67ae8584caa73bL; + v[10] = 0x3c6ef372fe94f82bL; + v[11] = 0xa54ff53a5f1d36f1L; + v[12] = t [0] ^ 0x510e527fade682d1L; + v[13] = t [1] ^ 0x9b05688c2b3e6c1fL; + v[14] = f [0] ^ 0x1f83d9abfb41bd6bL; + v[15] = f [1] ^ 0x5be0cd19137e2179L; + + // do the rounds + round_0(v, m); + round_1(v, m); + round_2(v, m); + round_3(v, m); + round_4(v, m); + round_5(v, m); + round_6(v, m); + round_7(v, m); + round_8(v, m); + round_9(v, m); + round_0(v, m); // round 10 is identical to round 0 + round_1(v, m); // round 11 is identical to round 1 + + // Update state vector h + h[0] ^= v[0] ^ v[8]; + h[1] ^= v[1] ^ v[9]; + h[2] ^= v[2] ^ v[10]; + h[3] ^= v[3] ^ v[11]; + h[4] ^= v[4] ^ v[12]; + h[5] ^= v[5] ^ v[13]; + h[6] ^= v[6] ^ v[14]; + h[7] ^= v[7] ^ v[15]; + + /* kaamil */ + } + private void round_0(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[0]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [1]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[2]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[3]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[4]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[5]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[6]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[7]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[8]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[9]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[10]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[11]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[12]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[13]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[14]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[15]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_1(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[14]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [10]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[4]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[8]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[9]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[15]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[13]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[6]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[1]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[12]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[0]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[2]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[11]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[7]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[5]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[3]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_2(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[11]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [8]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[12]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[0]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[5]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[2]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[15]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[13]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[10]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[14]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[3]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[6]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[7]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[1]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[9]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[4]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_3(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[7]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [9]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[3]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[1]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[13]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[12]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[11]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[14]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[2]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[6]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[5]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[10]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[4]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[0]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[15]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[8]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_4(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[9]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [0]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[5]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[7]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[2]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[4]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[10]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[15]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[14]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[1]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[11]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[12]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[6]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[8]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[3]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[13]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_5(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[2]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [12]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[6]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[10]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[0]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[11]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[8]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[3]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[4]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[13]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[7]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[5]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[15]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[14]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[1]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[9]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_6(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[12]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [5]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[1]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[15]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[14]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[13]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[4]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[10]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[0]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[7]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[6]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[3]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[9]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[2]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[8]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[11]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_7(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[13]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [11]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[7]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[14]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[12]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[1]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[3]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[9]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[5]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[0]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[15]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[4]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[8]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[6]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[2]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[10]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_8(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[6]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [15]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[14]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[9]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[11]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[3]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[0]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[8]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[12]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[2]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[13]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[7]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[1]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[4]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[10]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[5]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + private void round_9(final long[] v, final long[] m) { + v[ 0] = v[ 0] + v[ 4] + m[10]; + v[12] ^= v[ 0]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 0] = v[ 0] + v[ 4] + m [2]; + v[12] ^= v[ 0]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[ 8] = v[ 8] + v[12]; + v[ 4] ^= v[ 8]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + + v[ 1] = v[ 1] + v[ 5] + m[8]; + v[13] ^= v[ 1]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 1] = v[ 1] + v[ 5] + m[4]; + v[13] ^= v[ 1]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 9] = v[ 9] + v[13]; + v[ 5] ^= v[ 9]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 2] = v[ 2] + v[ 6] + m[7]; + v[14] ^= v[ 2]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 2] = v[ 2] + v[ 6] + m[6]; + v[14] ^= v[ 2]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[10] = v[10] + v[14]; + v[ 6] ^= v[10]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 3] = v[ 3] + v[ 7] + m[1]; + v[15] ^= v[ 3]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 3] = v[ 3] + v[ 7] + m[5]; + v[15] ^= v[ 3]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[11] = v[11] + v[15]; + v[ 7] ^= v[11]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 0] = v[ 0] + v[ 5] + m[15]; + v[15] ^= v[ 0]; + v[15] = ( v[15] << 32 ) | ( v[15] >>> 32 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] >>> 24 ) | ( v[ 5] << 40 ); + v[ 0] = v[ 0] + v[ 5] + m[11]; + v[15] ^= v[ 0]; + v[15] = ( v[15] >>> 16 ) | ( v[15] << 48 ); + v[10] = v[10] + v[15]; + v[ 5] ^= v[10]; + v[ 5] = ( v[ 5] << 1 ) | ( v[ 5] >>> 63 ); + + v[ 1] = v[ 1] + v[ 6] + m[9]; + v[12] ^= v[ 1]; + v[12] = ( v[12] << 32 ) | ( v[12] >>> 32 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] >>> 24 ) | ( v[ 6] << 40 ); + v[ 1] = v[ 1] + v[ 6] + + m[14]; + v[12] ^= v[ 1]; + v[12] = ( v[12] >>> 16 ) | ( v[12] << 48 ); + v[11] = v[11] + v[12]; + v[ 6] ^= v[11]; + v[ 6] = ( v[ 6] << 1 ) | ( v[ 6] >>> 63 ); + + v[ 2] = v[ 2] + v[ 7] + m[3]; + v[13] ^= v[ 2]; + v[13] = ( v[13] << 32 ) | ( v[13] >>> 32 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] >>> 24 ) | ( v[ 7] << 40 ); + v[ 2] = v[ 2] + v[ 7] + m[12]; + v[13] ^= v[ 2]; + v[13] = ( v[13] >>> 16 ) | ( v[13] << 48 ); + v[ 8] = v[ 8] + v[13]; + v[ 7] ^= v[ 8]; + v[ 7] = ( v[ 7] << 1 ) | ( v[ 7] >>> 63 ); + + v[ 3] = v[ 3] + v[ 4] + m[13]; + v[14] ^= v[ 3]; + v[14] = ( v[14] << 32 ) | ( v[14] >>> 32 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] >>> 24 ) | ( v[ 4] << 40 ); + v[ 3] = v[ 3] + v[ 4] + m[0]; + v[14] ^= v[ 3]; + v[14] = ( v[14] >>> 16 ) | ( v[14] << 48 ); + v[ 9] = v[ 9] + v[14]; + v[ 4] ^= v[ 9]; + v[ 4] = ( v[ 4] << 1 ) | ( v[ 4] >>> 63 ); + } + + //////////////////////////////////////////////////////////////////////// + /// Compression Kernel //////////////////////////////////////////// FINI + //////////////////////////////////////////////////////////////////////// + + /* TEMP - remove at will */ + public static class Debug { + public static void dumpState (Engine e, final String mark) { + System.out.format("-- MARK == @ %s @ ===========\n", mark); + dumpArray("register t", e.state.t); + dumpArray("register h", e.state.h); + dumpArray("register f", e.state.f); + dumpArray("register offset", new long[]{e.state.buflen}); + System.out.format("-- END MARK =================\n"); + } + public static void dumpArray (final String label, final long[] b) { + System.out.format ( "-- %s -- :\n{\n", label ); + for( int j = 0; j < b.length ; ++j ) { + System.out.format ( " [%2d] : %016X\n", j, b[j]); + } + System.out.format ( "}\n" ); + } + public static void dumpBuffer (final PrintStream out, final String label, final byte[] b) { + dumpBuffer(out, label, b, 0, b.length); + } + public static void dumpBuffer (final PrintStream out, final byte[] b) { + dumpBuffer(out, null, b, 0, b.length); + } + public static void dumpBuffer (final PrintStream out, final byte[] b, final int offset, final int len) { + dumpBuffer(out, null, b, offset, len); + } + public static void dumpBuffer (final PrintStream out, final String label, final byte[] b, final int offset, final int len) { + if(label != null) + out.format ( "-- %s -- :\n", label ); + out.format("{\n ", label); + for( int j = 0; j < len ; ++j ) { + out.format ("%02X", b[j + offset]); + if(j+1 < len) { + if ((j+1)%8==0) out.print("\n "); + else out.print(' '); + } + } + out.format("\n}\n"); + } + } + /* TEMP - remove at will */ + + // --------------------------------------------------------------------- + // Helper for assert error messages + // --------------------------------------------------------------------- + public static final class Assert { + public final static String exclusiveUpperBound = "'%s' %d is >= %d"; + public final static String inclusiveUpperBound = "'%s' %d is > %d"; + public final static String exclusiveLowerBound = "'%s' %d is <= %d"; + public final static String inclusiveLowerBound = "'%s' %d is < %d"; + static String assertFail(final String name, final T v, final String err, final T spec) { + new Exception().printStackTrace(); + return String.format(err, name, v, spec); + } + } + // --------------------------------------------------------------------- + // Little Endian Codecs (inlined in the compressor) + /* + * impl note: these are not library funcs and used in hot loops, so no + * null or bounds checks are performed. For our purposes, this is OK. + */ + // --------------------------------------------------------------------- + + public static class LittleEndian { + private static final byte[] hex_digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final byte[] HEX_digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + /** @return hex rep of byte (lower case). */ + static public String toHexStr (final byte[] b) { + return toHexStr (b, false); // because String class is slower. + } + static public String toHexStr (final byte[] b, boolean upperCase) { + final int len = b.length; + final byte[] digits = new byte[ len * 2 ]; + final byte[] hex_rep = upperCase ? HEX_digits : hex_digits ; + for (int i = 0; i < len; i++) { + digits [ i*2 ] = hex_rep [ (byte) (b[i] >> 4 & 0x0F) ]; + digits [ i*2+1 ] = hex_rep [ (byte) (b[i] & 0x0F) ]; + } + return new String(digits); + } + public static int readInt (final byte[] b, final int off) { + int v0 = ((int)b [ off ] & 0xFF ); + v0 |= ((int)b [ off + 1 ] & 0xFF ) << 8; + v0 |= ((int)b [ off + 2 ] & 0xFF ) << 16; + return v0 | ((int)b [ off + 3 ] ) << 24; + } + /** Little endian - byte[] to long */ + public static long readLong (final byte[] b, final int off) { + long v0 = ((long)b [ off ] & 0xFF ); + v0 |= ((long)b [ off + 1 ] & 0xFF ) << 8; + v0 |= ((long)b [ off + 2 ] & 0xFF ) << 16; + v0 |= ((long)b [ off + 3 ] & 0xFF ) << 24; + v0 |= ((long)b [ off + 4 ] & 0xFF ) << 32; + v0 |= ((long)b [ off + 5 ] & 0xFF ) << 40; + v0 |= ((long)b [ off + 6 ] & 0xFF ) << 48; + return v0 | ((long)b [ off + 7 ] ) << 56; + } + /** Little endian - long to byte[] */ + public static void writeLong (long v, final byte[] b, final int off) { + b [ off ] = (byte) v; v >>>= 8; + b [ off + 1 ] = (byte) v; v >>>= 8; + b [ off + 2 ] = (byte) v; v >>>= 8; + b [ off + 3 ] = (byte) v; v >>>= 8; + b [ off + 4 ] = (byte) v; v >>>= 8; + b [ off + 5 ] = (byte) v; v >>>= 8; + b [ off + 6 ] = (byte) v; v >>>= 8; + b [ off + 7 ] = (byte) v; + } + /** Little endian - int to byte[] */ + public static void writeInt (int v, final byte[] b, final int off) { + b [ off ] = (byte) v; v >>>= 8; + b [ off + 1 ] = (byte) v; v >>>= 8; + b [ off + 2 ] = (byte) v; v >>>= 8; + b [ off + 3 ] = (byte) v; + } + } + } + // --------------------------------------------------------------------- + // digest parameter (block) + // --------------------------------------------------------------------- + /** Blake2b configuration parameters block per spec */ + // REVU: need to review a revert back to non-lazy impl TODO: do & bench + public static class Param implements AlgorithmParameterSpec { + interface Xoff { + int digest_length = 0; + int key_length = 1; + int fanout = 2; + int depth = 3; + int leaf_length = 4; + int node_offset = 8; + int node_depth = 16; + int inner_length = 17; + int reserved = 18; + int salt = 32; + int personal = 48; + } + public interface Default { + byte digest_length = Spec.max_digest_bytes; + byte key_length = 0; + byte fanout = 1; + byte depth = 1; + int leaf_length = 0; + long node_offset = 0; + byte node_depth = 0; + byte inner_length = 0; + } + /** default bytes of Blake2b parameter block */ + final static byte[] default_bytes = new byte[ Spec.param_bytes ]; + /** initialize default_bytes */ + static { + default_bytes [ Xoff.digest_length ] = Default.digest_length; + default_bytes [ Xoff.key_length ] = Default.key_length; + default_bytes [ Xoff.fanout ] = Default.fanout; + default_bytes [ Xoff.depth ] = Default.depth; + /* def. leaf_length is 0 fill and already set by new byte[] */ + /* def. node_offset is 0 fill and already set by new byte[] */ + default_bytes [ Xoff.node_depth ] = Default.node_depth; + default_bytes [ Xoff.inner_length] = Default.inner_length; + /* def. salt is 0 fill and already set by new byte[] */ + /* def. personal is 0 fill and already set by new byte[] */ + } + /** default Blake2b h vector */ + final static long[] default_h = new long [ Spec.state_space_len ]; + static { + default_h [0] = readLong( default_bytes, 0 ); + default_h [1] = readLong( default_bytes, 8 ); + default_h [2] = readLong( default_bytes, 16 ); + default_h [3] = readLong( default_bytes, 24 ); + default_h [4] = readLong( default_bytes, 32 ); + default_h [5] = readLong( default_bytes, 40 ); + default_h [6] = readLong( default_bytes, 48 ); + default_h [7] = readLong( default_bytes, 56 ); + + default_h [0] ^= Spec.IV [0]; + default_h [1] ^= Spec.IV [1]; + default_h [2] ^= Spec.IV [2]; + default_h [3] ^= Spec.IV [3]; + default_h [4] ^= Spec.IV [4]; + default_h [5] ^= Spec.IV [5]; + default_h [6] ^= Spec.IV [6]; + default_h [7] ^= Spec.IV [7]; + } + + /** */ + private boolean hasKey = false; + /** not sure how to make this secure - TODO */ + private byte[] key_bytes = null; + /** */ + private byte[] bytes = null; + /** */ + private final long[] h = new long [ Spec.state_space_len ]; + + /** */ + public Param() { + System.arraycopy( default_h, 0, h, 0, Spec.state_space_len ); + } + /** */ + public long[] initialized_H () { + return h; + } + /** package only - copy returned - do not use in functional loops */ + public byte[] getBytes() { + lazyInitBytes(); + byte[] copy = new byte[ bytes.length ]; + System.arraycopy( bytes, 0, copy, 0, bytes.length ); + return copy; + } + + final byte getByteParam (final int xoffset) { + byte[] _bytes = bytes; + if(_bytes == null) _bytes = Param.default_bytes; + return _bytes[ xoffset]; + } + final int getIntParam (final int xoffset) { + byte[] _bytes = bytes; + if(_bytes == null) _bytes = Param.default_bytes; + return readInt ( _bytes, xoffset); + } + final long getLongParam (final int xoffset) { + byte[] _bytes = bytes; + if(_bytes == null) _bytes = Param.default_bytes; + return readLong ( _bytes, xoffset); + } + // TODO same for tree params depth, fanout, inner, node-depth, node-offset + public final int getDigestLength() { + return (int) getByteParam ( Xoff.digest_length ); + } + public final int getKeyLength() { + return (int) getByteParam ( Xoff.key_length ); + } + public final int getFanout() { + return (int) getByteParam ( Xoff.fanout ); + } + public final int getDepth() { + return (int) getByteParam ( Xoff.depth ); + } + public final int getLeafLength() { + return getIntParam ( Xoff.leaf_length ); + } + public final long getNodeOffset() { + return getLongParam ( Xoff.node_offset ); + } + public final int getNodeDepth() { + return (int) getByteParam ( Xoff.node_depth ); + } + public final int getInnerLength() { + return (int) getByteParam ( Xoff.inner_length ); + } + + public final boolean hasKey() { return this.hasKey; } + + @Override public Param clone() { + final Param clone = new Param(); + System.arraycopy(this.h, 0, clone.h, 0, h.length); + clone.lazyInitBytes(); + System.arraycopy(this.bytes, 0, clone.bytes, 0, this.bytes.length); + + if(this.hasKey){ + clone.hasKey = this.hasKey; + clone.key_bytes = new byte [Spec.max_key_bytes * 2]; + System.arraycopy(this.key_bytes, 0, clone.key_bytes, 0, this.key_bytes.length); + } + return clone; + } + //////////////////////////////////////////////////////////////////////// + /// lazy setters - write directly to the bytes image of param block //// + //////////////////////////////////////////////////////////////////////// + final void lazyInitBytes () { + if( bytes == null ) { + bytes = new byte [ Spec.param_bytes ]; + System.arraycopy(Param.default_bytes, 0, bytes, 0, Spec.param_bytes); + } + } + /* 0-7 inclusive */ + public final Param setDigestLength(int len) { + assert len > 0 : assertFail("len", len, exclusiveLowerBound, 0); + assert len <= Spec.max_digest_bytes : assertFail("len", len, inclusiveUpperBound, Spec.max_digest_bytes); + + lazyInitBytes(); + bytes[ Xoff.digest_length ] = (byte) len; + h[ 0 ] = readLong( bytes, 0 ); + h[ 0 ] ^= Spec.IV [ 0 ]; + return this; + } + public final Param setKey (final Key key) { + assert key != null : "key is null"; + final byte[] keybytes = key.getEncoded(); + assert keybytes != null : "key.encoded() is null"; + + return this.setKey(keybytes); + } + public final Param setKey (final byte[] key) { + assert key != null : "key is null"; + assert key.length >= 0 : assertFail("key.length", key.length, inclusiveUpperBound, 0); + assert key.length <= Spec.max_key_bytes : assertFail("key.length", key.length, inclusiveUpperBound, Spec.max_key_bytes); + + // zeropad keybytes + this.key_bytes = new byte [Spec.max_key_bytes * 2]; + System.arraycopy ( key, 0, this.key_bytes, 0, key.length ); + lazyInitBytes(); + bytes[ Xoff.key_length ] = (byte) key.length; // checked c ref; this is correct + h[ 0 ] = readLong( bytes, 0 ); + h[ 0 ] ^= Spec.IV [ 0 ]; + this.hasKey = true; + return this; + } + public final Param setFanout(int fanout) { + assert fanout > 0 : assertFail("fanout", fanout, exclusiveLowerBound, 0); + + lazyInitBytes(); + bytes[ Xoff.fanout ] = (byte) fanout; + h[ 0 ] = readLong( bytes, 0 ); + h[ 0 ] ^= Spec.IV [ 0 ]; + return this; + } + public final Param setDepth(int depth) { + assert depth > 0 : assertFail("depth", depth, exclusiveLowerBound, 0); + + lazyInitBytes(); + bytes[ Xoff.depth ] = (byte) depth; + h[ 0 ] = readLong( bytes, 0 ); + h[ 0 ] ^= Spec.IV [ 0 ]; + return this; + } + public final Param setLeafLength(int leaf_length) { + assert leaf_length >= 0 : assertFail("leaf_length", leaf_length, inclusiveLowerBound, 0); + + lazyInitBytes(); + writeInt (leaf_length, bytes, Xoff.leaf_length); + h[ 0 ] = readLong( bytes, 0 ); + h[ 0 ] ^= Spec.IV [ 0 ]; + return this; + } + + /* 8-15 inclusive */ + public final Param setNodeOffset(long node_offset) { + assert node_offset >= 0 : assertFail("node_offset", node_offset, inclusiveLowerBound, 0); + + lazyInitBytes(); + writeLong(node_offset, bytes, Xoff.node_offset); + h[ 1 ] = readLong( bytes, Xoff.node_offset ); + h[ 1 ] ^= Spec.IV [ 1 ]; + return this; + } + + /* 16-23 inclusive */ + public final Param setNodeDepth(int node_depth) { + assert node_depth >= 0 : assertFail("node_depth", node_depth, inclusiveLowerBound, 0); + + lazyInitBytes(); + bytes[ Xoff.node_depth ] = (byte) node_depth; + h[ 2 ] = readLong( bytes, Xoff.node_depth ); + h[ 2 ] ^= Spec.IV [ 2 ]; + h[ 3 ] = readLong( bytes, Xoff.node_depth + 8); + h[ 3 ] ^= Spec.IV [ 3 ]; + return this; + } + public final Param setInnerLength(int inner_length) { + assert inner_length >= 0 : assertFail("inner_length", inner_length, inclusiveLowerBound, 0); + + lazyInitBytes(); + bytes[ Xoff.inner_length] = (byte) inner_length; + h[ 2 ] = readLong( bytes, Xoff.node_depth ); + h[ 2 ] ^= Spec.IV [ 2 ]; + h[ 3 ] = readLong( bytes, Xoff.node_depth + 8); + h[ 3 ] ^= Spec.IV [ 3 ]; + return this; + } + + /* 24-31 masked by reserved and remain unchanged */ + + /* 32-47 inclusive */ + public final Param setSalt(final byte[] salt) { + assert salt != null : "salt is null"; + assert salt.length <= Spec.max_salt_bytes : assertFail("salt.length", salt.length, inclusiveUpperBound, Spec.max_salt_bytes); + + lazyInitBytes(); + Arrays.fill ( bytes, Xoff.salt, Xoff.salt + Spec.max_salt_bytes, (byte)0); + System.arraycopy( salt, 0, bytes, Xoff.salt, salt.length ); + h[ 4 ] = readLong( bytes, Xoff.salt ); + h[ 4 ] ^= Spec.IV [ 4 ]; + h[ 5 ] = readLong( bytes, Xoff.salt + 8 ); + h[ 5 ] ^= Spec.IV [ 5 ]; + return this; + } + + /* 48-63 inclusive */ + public final Param setPersonal(byte[] personal) { + assert personal != null : "personal is null"; + assert personal.length <= Spec.max_personalization_bytes : assertFail("personal.length", personal.length, inclusiveUpperBound, Spec.max_personalization_bytes); + + lazyInitBytes(); + Arrays.fill ( bytes, Xoff.personal, Xoff.personal + Spec.max_personalization_bytes, (byte)0); + System.arraycopy( personal, 0, bytes, Xoff.personal, personal.length ); + h[ 6 ] = readLong( bytes, Xoff.personal ); + h[ 6 ] ^= Spec.IV [ 6 ]; + h[ 7 ] = readLong( bytes, Xoff.personal + 8 ); + h[ 7 ] ^= Spec.IV [ 7 ]; + return this; + } + //////////////////////////////////////////////////////////////////////// + /// lazy setters /////////////////////////////////////////////////// END + //////////////////////////////////////////////////////////////////////// + } +} diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoData.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoData.java new file mode 100644 index 0000000000..afe9f7b101 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoData.java @@ -0,0 +1,122 @@ +package com.tangem.domain.wallet.cardano; + +import android.os.Bundle; +import android.util.Log; + +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.CoinEngine; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class CardanoData extends CoinData { + public CardanoData() { + super(); + } + + private Long balance; + + public String getUnspentInputsDescription() { + try { +// int gatheredUnspents = 0; + if( unspentOutputs ==null ) return ""; +// for (int i = 0; i < unspentOutputs.size(); i++) { +// if (unspentOutputs.get(i).Raw != null && unspentOutputs.get(i).Raw.length() > 1) gatheredUnspents++; +// } + return String.valueOf(unspentOutputs.size()) + " unspents";// (" + String.valueOf(gatheredUnspents) + " received)"; + } + catch (Exception e) + { + e.printStackTrace(); + return ""; + } + } + + public static class UnspentOutput { + public String txID; + public Long Amount; + public Integer Index; + + public Bundle getAsBundle() { + Bundle B = new Bundle(); + B.putString("txID", txID); + B.putLong("Amount", Amount); + B.putInt("Index", Index); + return B; + } + + public void loadFromBundle(Bundle B) { + txID = B.getString("txID"); + Amount = B.getLong("Amount"); + Index = B.getInt("Index"); + } + } + + + private List unspentOutputs = null; + + public List getUnspentOutputs() { + if (unspentOutputs == null) unspentOutputs = new ArrayList<>(); + return unspentOutputs; + } + + @Override + public void loadFromBundle(Bundle B) { + super.loadFromBundle(B); + + if (B.containsKey("Balance")) balance = B.getLong("Balance"); + else balance = null; + if (B.containsKey("UnspentTransactions")) { + unspentOutputs = new ArrayList<>(); + Bundle BB = B.getBundle("UnspentTransactions"); + Integer i = 0; + while (BB.containsKey(i.toString())) { + UnspentOutput t = new UnspentOutput(); + t.loadFromBundle(BB.getBundle(i.toString())); + unspentOutputs.add(t); + i++; + } + } + } + + @Override + public void saveToBundle(Bundle B) { + super.saveToBundle(B); + try { + if (unspentOutputs != null) { + Bundle BB = new Bundle(); + for (Integer i = 0; i < unspentOutputs.size(); i++) { + BB.putBundle(i.toString(), unspentOutputs.get(i).getAsBundle()); + } + B.putBundle("UnspentTransactions", BB); + } + if (balance != null) B.putLong("Balance", balance); + } catch (Exception e) { + Log.e("Can't save to bundle ", e.getMessage()); + } + } + + @Override + public void clearInfo() { + super.clearInfo(); + balance = null; + unspentOutputs = null; + } + + public CoinEngine.InternalAmount getBalanceInInternalUnits() { + return new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace"); + } + + public void setBalance(Long balance) { + this.balance = balance; + } + + public Long getBalance() { + return balance; + } + + public boolean hasBalanceInfo() { + return balance != null; + } +} diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java new file mode 100644 index 0000000000..e022fba0c0 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -0,0 +1,665 @@ +package com.tangem.domain.wallet.cardano; + +import android.net.Uri; +import android.text.InputFilter; +import android.util.Log; + +import com.tangem.data.network.ServerApiAdalite; +import com.tangem.data.network.model.AdaliteResponse; +import com.tangem.data.network.model.AdaliteResponseUtxo; +import com.tangem.data.network.model.UtxoData; +import com.tangem.domain.wallet.BTCUtils; +import com.tangem.domain.wallet.BalanceValidator; +import com.tangem.domain.wallet.Base58; +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.CoinEngine; +import com.tangem.domain.wallet.TangemContext; +import com.tangem.tangemcard.data.TangemCard; +import com.tangem.tangemcard.reader.CardProtocol; +import com.tangem.tangemcard.tasks.SignTask; +import com.tangem.tangemcard.util.Util; +import com.tangem.util.DecimalDigitsInputFilter; +import com.tangem.wallet.R; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.zip.CRC32; + +import co.nstant.in.cbor.CborBuilder; +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.CborEncoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.builder.ArrayBuilder; +import co.nstant.in.cbor.model.DataItem; + +import static com.tangem.domain.wallet.Base58.decodeBase58; +import static com.tangem.domain.wallet.Base58.encodeBase58; + +public class CardanoEngine extends CoinEngine { + + private static final String TAG = CardanoEngine.class.getSimpleName(); + + private static final long protocolMagic = 764824073; + + public CardanoData coinData = null; + + public CardanoEngine(TangemContext context) throws Exception { + super(context); + if (context.getCoinData() == null) { + coinData = new CardanoData(); + context.setCoinData(coinData); + } else if (context.getCoinData() instanceof CardanoData) { + coinData = (CardanoData) context.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for CardanoEngine"); + } + } + + public CardanoEngine() { + 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() { + return false; + } + + @Override + public String getBalanceHTML() { + Amount balance = getBalance(); + if (balance != null) { + return balance.toDescriptionString(getDecimals()); + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + return "ADA"; + } + + @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(); + } + + @Override + 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 if (coinData.getUnspentOutputs().size() == 0) { + ctx.setMessage(R.string.please_wait_for_confirmation); + } else { + return true; + } + return false; + } + + @Override + public String getFeeCurrency() { + return "BTC"; + } + + @Override + public boolean validateAddress(String address) { + if (address == null || address.isEmpty()) { + return false; + } + + byte[] decAddress = Base58.decodeBase58(address); + + if (decAddress == null || decAddress.length == 0) { + return false; + } + + ByteArrayInputStream bais = new ByteArrayInputStream(decAddress); + try { + List list = new CborDecoder(bais).decode(); +// DataItem[] array = (DataItem[]) list.toArray(); TODO: Complete +// byte[] addressDataEncoded = (byte[]) array[0].getTag().getValue(); +// int crc32Checksum = (int) array[1].getTag().getValue(); +// CRC32 Checksum = new CRC32(); +// Checksum.update(); +// if (crc32Checksum !== CRC32(addressDataEncoded)) { +// return false +// } + } catch (CborException e) { + return false; + } + + + if (address.length() < 30) { //TODO: Check + return false; + } + + return true; + } + + @Override + public boolean isNeedCheckNode() { + return true; + } + + @Override + public Uri getWalletExplorerUri() { + return Uri.parse("https://cardanoexplorer.com/address/" + ctx.getCoinData().getWallet()); + } + + @Override + public Uri getShareWalletUri() { + return Uri.parse(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.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()); + } + + @Override + public String calculateAddress(byte[] pkUncompressed) throws CborException { + + final Blake2b blake2b = Blake2b.Digest.newInstance(28); + byte[] pkHash = blake2b.digest(pkUncompressed); + + //pkHash + attributes + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new CborEncoder(baos).encode(new CborBuilder() + .addArray() + .add(pkHash) + .addMap()//additional attributes + .end() + .add(0)//address type + .end() + .build()); + byte[] addr = baos.toByteArray(); + + final CRC32 crc32 = new CRC32(); + crc32.update(addr); + int checksum = (int) crc32.getValue(); + + DataItem addrItem = new CborBuilder().add(addr).build().get(0); + addrItem.setTag(24); + + //addr + checksum + baos.reset(); + new CborEncoder(baos).encode(new CborBuilder() + .addArray() + .add(addrItem) + .add(checksum) + .end() + .build()); + + byte[] hexAddress = baos.toByteArray(); + return encodeBase58(hexAddress); + } + + @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, "Lovelace"); + } + + @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), "Lovelace"); + } + + @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 CardanoData(); + } + + @Override + public String getUnspentInputsDescription() { + return coinData.getUnspentInputsDescription(); + } + + @Override + public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { + checkBlockchainDataExists(); + + String myAddress = ctx.getCoinData().getWallet(); + byte[] pbKey = ctx.getCard().getWalletPublicKey(); + List utxoList = coinData.getUnspentOutputs(); + long fullAmount = coinData.getBalance(); + +// long fullAmount = 0; TODO: check +// for (int i = 0; i < unspentOutputs.size(); ++i) { +// fullAmount += unspentOutputs.get(i).value; +// } + + long fees = convertToInternalAmount(feeValue).longValueExact(); + long amount = convertToInternalAmount(amountValue).longValueExact(); + long change = fullAmount - amount; + if (IncFee) { + amount = amount - fees; + } else { + change = change - fees; + } + + final long amountFinal = amount; + final long changeFinal = change; + + if (amount + fees > fullAmount) { + throw new CardProtocol.TangemException_WrongAmount(String.format("Balance (%d) < change (%d) + amount (%d)", fullAmount, change, amount)); + } + + CborBuilder cborBuilder = new CborBuilder(); + ArrayBuilder inputsArray = cborBuilder.startArray(); + ArrayBuilder outputsArray = cborBuilder.startArray(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + //Inputs + for (CardanoData.UnspentOutput utxo : utxoList) { + baos.reset(); + + //txID + inputPos + new CborEncoder(baos).encode(new CborBuilder() + .addArray() + .add(Util.fromHexString(utxo.txID)) + .add(utxo.Index) + .end() + .build()); + byte[] input = baos.toByteArray(); + + DataItem inputItem = new CborBuilder().add(input).build().get(0); + inputItem.setTag(24); + + //input type + input + inputsArray + .addArray() + .add(0) + .add(inputItem) + .end(); + } + + //1st output + DataItem targetAddressItem = new CborDecoder(new ByteArrayInputStream(decodeBase58(targetAddress))).decode().get(0); + outputsArray + .addArray() + .add(targetAddressItem) + .add(amountFinal) + .end(); + //2nd output (optional) + if (changeFinal > 0) { + DataItem myAddressItem = new CborDecoder(new ByteArrayInputStream(decodeBase58(myAddress))).decode().get(0); + outputsArray + .addArray() + .add(myAddressItem) + .add(changeFinal) + .end(); + } + + inputsArray.end(); + outputsArray.end(); + baos.reset(); + new CborEncoder(baos).encode(cborBuilder.build()); + byte[] txBody = baos.toByteArray(); + + final Blake2b blake2b = Blake2b.Digest.newInstance(28); + byte[] txHash = blake2b.digest(txBody); + + baos.reset(); + new CborEncoder(baos).encode(new CborBuilder().add(protocolMagic).build()); + byte[] magic = baos.toByteArray(); + + //dataToSign prefix + baos.reset(); + baos.write(new byte[] {(byte) 0x01}); + baos.write(magic); + baos.write(new byte[]{(byte) 0x58, (byte) 0x20}); + + baos.write(txHash); + byte[] dataToSign = baos.toByteArray(); + + + + return new SignTask.TransactionToSign() { + + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw; + } + + @Override + public byte[][] getHashesToSign() throws Exception { + byte[][] dataForSign = new byte[1][]; + dataForSign[0] = dataToSign; + return dataForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing Raw Data is not supported for Cardano"); + } + + @Override + public String getHashAlgToSign() { + return "sha-256x2"; + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Issuer validation not supported!"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + //pubkey + signature + baos.reset(); + new CborEncoder(baos).encode(new CborBuilder() + .addArray() + .add(ctx.getCard().getCardPublicKey()) + .add(signFromCard) + .end() + .build()); + byte[] witnessBody = baos.toByteArray(); + DataItem witnessBodyItem = new CborBuilder().add(witnessBody).build().get(0); + witnessBodyItem.setTag(24); + + //witness type + witness body + baos.reset(); + new CborEncoder(baos).encode(new CborBuilder() + .addArray() + .add(0) + .add(witnessBodyItem) + .end() + .build()); + byte[] witness = baos.toByteArray(); + + baos.reset(); + new CborEncoder(baos).encode(new CborBuilder() + .addArray() + .add(txBody) + .add(witness) + .end() + .build()); + byte[] txForSend = baos.toByteArray(); + return txForSend; + } + }; + } + + @Override + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiAdalite serverApiAdalite = new ServerApiAdalite(); + + ServerApiAdalite.ResponseListener adaliteListener = new ServerApiAdalite.ResponseListener() { + @Override + public void onSuccess(String method, AdaliteResponse adaliteResponse) { + Log.i(TAG, "onSuccess: " + method); + try { + String walletAddress = adaliteResponse.getRight().getCaAddress(); + if (!walletAddress.equals(coinData.getWallet())) { + // todo - check + throw new Exception("Invalid wallet address in answer!"); + } + coinData.setBalanceReceived(true); + coinData.setBalance(adaliteResponse.getRight().getCaBalance().getGetCoin()); + coinData.setValidationNodeDescription(ServerApiAdalite.lastNode); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception"); + } + + if (serverApiAdalite.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + + @Override + public void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo) { + Log.i(TAG, "onSuccess: " + method); + try { + coinData.getUnspentOutputs().clear(); + for (UtxoData utxo : adaliteResponseUtxo.getRight()) { + CardanoData.UnspentOutput unspentOutput = new CardanoData.UnspentOutput(); + unspentOutput.txID = utxo.getCuId(); + unspentOutput.Amount = utxo.getCuCoins().getGetCoin(); + unspentOutput.Index = utxo.getCuOutIndex(); + coinData.getUnspentOutputs().add(unspentOutput); + } + + } catch (Exception e) { + e.printStackTrace(); + } + + if (serverApiAdalite.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + Log.e(TAG, "FAIL INSIGHT_UNSPENT_OUTPUTS Exception"); + } + } + + @Override + public void onFail(String method, String message) { + Log.i(TAG, "onFail: " + method + " " + message); + ctx.setError(message); + if (serverApiAdalite.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(false); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + }; + + serverApiAdalite.setResponseListener(adaliteListener); + + serverApiAdalite.requestData(ServerApiAdalite.ADALITE_ADDRESS, ctx.getCoinData().getWallet(), ""); + serverApiAdalite.requestData(ServerApiAdalite.ADALITE_UNSPENT_OUTPUTS, ctx.getCoinData().getWallet(), ""); + } + + @Override + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + coinData.minFee = new Amount(new BigDecimal(0.2).setScale(6, RoundingMode.DOWN), ctx.getBlockchain().getCurrency()); + coinData.normalFee = new Amount(new BigDecimal(0.2).setScale(6, RoundingMode.DOWN), ctx.getBlockchain().getCurrency()); + coinData.maxFee = new Amount(new BigDecimal(0.2).setScale(6, RoundingMode.DOWN), ctx.getBlockchain().getCurrency()); + + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + final ServerApiAdalite serverApiAdalite = new ServerApiAdalite(); + final String txStr = BTCUtils.toHex(txForSend); + + final ServerApiAdalite.ResponseListener responseListener = new ServerApiAdalite.ResponseListener() { + @Override + public void onSuccess(String method, AdaliteResponse adaliteResponse) { + if (method.equals(ServerApiAdalite.ADALITE_SEND)) { + String resultString = adaliteResponse.toString(); + try { + if (resultString.isEmpty()) { + ctx.setError("No response from node"); + blockchainRequestsCallbacks.onComplete(false); + } else { // TODO: Make check for a valid send response + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } + } catch (Exception e) { + if (e.getMessage() != null) { + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } else { + ctx.setError(e.getClass().getName()); + blockchainRequestsCallbacks.onComplete(false); + Log.e(TAG, resultString); + } + } + } + } + + @Override + public void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo) { + Log.e(TAG, "Wrong response type for ADALITE_SEND"); + ctx.setError("Wrong response type for ADALITE_SEND"); + } + + @Override + public void onFail(String method, String message) { + if (!serverApiAdalite.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + serverApiAdalite.setResponseListener(responseListener); + + serverApiAdalite.requestData(ServerApiAdalite.ADALITE_SEND, "", txStr); + } +} diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoUtils.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoUtils.java new file mode 100644 index 0000000000..a32e926e0b --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoUtils.java @@ -0,0 +1,4 @@ +package com.tangem.domain.wallet.cardano; + +public final class CardanoUtils { +} diff --git a/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java b/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java index 32261ff2c9..94b4276189 100644 --- a/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java @@ -253,7 +253,7 @@ public class DucatusEngine extends BtcEngine { return false; } - if (coinData.isBalanceReceived() && coinData.isBalanceEqual()) { + if (coinData.isBalanceReceived()) {// && coinData.isBalanceEqual()) { TODO:check balanceValidator.setScore(100); balanceValidator.setFirstLine("Verified balance"); balanceValidator.setSecondLine("Balance confirmed in blockchain"); diff --git a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt index 3a0117b6fc..cc1184277b 100644 --- a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt +++ b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt @@ -734,6 +734,7 @@ class LoadedWallet : androidx.fragment.app.Fragment(), NfcAdapter.ReaderCallback Blockchain.Litecoin -> "litecoin" Blockchain.Rootstock -> "bitcoin" Blockchain.RootstockToken -> "bitcoin" + Blockchain.Cardano -> "cardano" //TODO:check else -> { throw Exception("Can''t get rate for blockchain " + ctx.blockchainName) } From a717411892b4c3564352dcc2953286e52e7c65d2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Feb 2019 13:01:28 +0300 Subject: [PATCH 02/11] Updated on 2026-08-14 --- .../java/com/tangem/domain/wallet/cardano/CardanoEngine.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index e022fba0c0..9b3e4d2436 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -303,7 +303,7 @@ public class CardanoEngine extends CoinEngine { final CRC32 crc32 = new CRC32(); crc32.update(addr); - int checksum = (int) crc32.getValue(); + long checksum = crc32.getValue(); DataItem addrItem = new CborBuilder().add(addr).build().get(0); addrItem.setTag(24); @@ -550,7 +550,7 @@ public class CardanoEngine extends CoinEngine { coinData.setValidationNodeDescription(ServerApiAdalite.lastNode); } catch (Exception e) { e.printStackTrace(); - Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception"); + Log.e(TAG, "FAIL ADALITE_ADDRESS Exception"); } if (serverApiAdalite.isRequestsSequenceCompleted()) { From b036295cd5c30584e1aab9d844e33b6863a789df Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Feb 2019 13:13:18 +0300 Subject: [PATCH 03/11] Updated on 2026-08-14 --- .../com/tangem/data/network/AdaliteApi.java | 3 ++- .../tangem/data/network/ServerApiAdalite.java | 7 ++++--- .../data/network/model/AdaliteBody.java | 9 +++++++++ .../domain/wallet/cardano/CardanoEngine.java | 20 +++++++++++++------ 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/tangem/data/network/model/AdaliteBody.java diff --git a/app/src/main/java/com/tangem/data/network/AdaliteApi.java b/app/src/main/java/com/tangem/data/network/AdaliteApi.java index a081ba8862..c8f84c7283 100644 --- a/app/src/main/java/com/tangem/data/network/AdaliteApi.java +++ b/app/src/main/java/com/tangem/data/network/AdaliteApi.java @@ -1,5 +1,6 @@ package com.tangem.data.network; +import com.tangem.data.network.model.AdaliteBody; import com.tangem.data.network.model.AdaliteResponse; import com.tangem.data.network.model.AdaliteResponseUtxo; @@ -25,5 +26,5 @@ public interface AdaliteApi { @Headers("Content-Type: application/json") @POST(ServerApiAdalite.ADALITE_SEND) - Call adaliteSend(@Body String rawTx ); + Call adaliteSend(@Body AdaliteBody adaliteBody); } diff --git a/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java b/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java index f5192f2034..0dd2db15c2 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java @@ -2,6 +2,7 @@ package com.tangem.data.network; import android.util.Log; +import com.tangem.data.network.model.AdaliteBody; import com.tangem.data.network.model.AdaliteResponse; import com.tangem.data.network.model.AdaliteResponseUtxo; @@ -19,7 +20,7 @@ public class ServerApiAdalite { public static final String ADALITE_ADDRESS = "/api/addresses/summary/{address}"; public static final String ADALITE_UNSPENT_OUTPUTS = "/api/bulk/addresses/utxo"; // public static final String ADALITE_TRANSACTION = "/api/txs/raw/{txId}}"; - public static final String ADALITE_SEND = "/tx/send"; + public static final String ADALITE_SEND = "/api/v2/txs/signed"; private int requestsCount = 0; @@ -46,7 +47,7 @@ public class ServerApiAdalite { public void requestData(String method, String wallet, String tx) { requestsCount++; - String adaliteURL = "https://explorer.adalite.io"; //TODO: make random selection + String adaliteURL = "https://explorer2.adalite.io"; //TODO: make random selection this.lastNode = adaliteURL; //TODO: show node instead of URL Retrofit retrofitAdalite = new Retrofit.Builder() @@ -90,7 +91,7 @@ public class ServerApiAdalite { // break; case ADALITE_SEND: - call = adaliteApi.adaliteSend(tx); + call = adaliteApi.adaliteSend(new AdaliteBody(tx)); break; default: diff --git a/app/src/main/java/com/tangem/data/network/model/AdaliteBody.java b/app/src/main/java/com/tangem/data/network/model/AdaliteBody.java new file mode 100644 index 0000000000..a8208d01cf --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/AdaliteBody.java @@ -0,0 +1,9 @@ +package com.tangem.data.network.model; + +public class AdaliteBody { + private String signedTx; + + public AdaliteBody(String signedTx) { + this.signedTx = signedTx; + } +} diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index 9b3e4d2436..939266a6b1 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -2,6 +2,7 @@ package com.tangem.domain.wallet.cardano; import android.net.Uri; import android.text.InputFilter; +import android.util.Base64; import android.util.Log; import com.tangem.data.network.ServerApiAdalite; @@ -129,7 +130,7 @@ public class CardanoEngine extends CoinEngine { @Override public String getFeeCurrency() { - return "BTC"; + return "ADA"; } @Override @@ -396,8 +397,9 @@ public class CardanoEngine extends CoinEngine { } CborBuilder cborBuilder = new CborBuilder(); - ArrayBuilder inputsArray = cborBuilder.startArray(); - ArrayBuilder outputsArray = cborBuilder.startArray(); + ArrayBuilder txArray = cborBuilder.addArray(); + ArrayBuilder> inputsArray = txArray.startArray(); + ArrayBuilder> outputsArray = txArray.startArray(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); //Inputs @@ -443,11 +445,14 @@ public class CardanoEngine extends CoinEngine { inputsArray.end(); outputsArray.end(); + + txArray.addMap().end(); + baos.reset(); new CborEncoder(baos).encode(cborBuilder.build()); byte[] txBody = baos.toByteArray(); - final Blake2b blake2b = Blake2b.Digest.newInstance(28); + final Blake2b blake2b = Blake2b.Digest.newInstance(32);//28? byte[] txHash = blake2b.digest(txBody); baos.reset(); @@ -500,7 +505,7 @@ public class CardanoEngine extends CoinEngine { baos.reset(); new CborEncoder(baos).encode(new CborBuilder() .addArray() - .add(ctx.getCard().getCardPublicKey()) + .add(ctx.getCard().getWalletPublicKey()) .add(signFromCard) .end() .build()); @@ -526,6 +531,8 @@ public class CardanoEngine extends CoinEngine { .end() .build()); byte[] txForSend = baos.toByteArray(); + notifyOnNeedSendTransaction(txForSend); + String hex = BTCUtils.toHex(txForSend); return txForSend; } }; @@ -616,7 +623,8 @@ public class CardanoEngine extends CoinEngine { @Override public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { final ServerApiAdalite serverApiAdalite = new ServerApiAdalite(); - final String txStr = BTCUtils.toHex(txForSend); + final String txStr = Base64.encodeToString(txForSend, Base64.NO_WRAP); + // BTCUtils.toHex(txForSend); final ServerApiAdalite.ResponseListener responseListener = new ServerApiAdalite.ResponseListener() { @Override From 21538aee90e79c5191694beb060ad263c2dca160 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Feb 2019 19:05:03 +0300 Subject: [PATCH 04/11] Updated on 2026-08-14 --- .../com/tangem/domain/wallet/CoinEngine.java | 3 +- .../domain/wallet/cardano/CardanoEngine.java | 69 ++++++++++++++----- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java index 702dcb8097..285b9e06fd 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java @@ -6,6 +6,7 @@ import android.text.InputFilter; import com.tangem.tangemcard.reader.CardProtocol; import com.tangem.tangemcard.tasks.SignTask; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; @@ -216,7 +217,7 @@ public abstract class CoinEngine { public abstract boolean validateAddress(String address); - public abstract String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException, CborException; + public abstract String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException, CborException, IOException; public abstract Amount convertToAmount(InternalAmount internalAmount) throws Exception; diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index 939266a6b1..a71c452502 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -22,8 +22,12 @@ import com.tangem.tangemcard.util.Util; import com.tangem.util.DecimalDigitsInputFilter; import com.tangem.wallet.R; +import org.spongycastle.crypto.Digest; +import org.spongycastle.crypto.util.DigestFactory; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; @@ -285,10 +289,34 @@ public class CardanoEngine extends CoinEngine { } @Override - public String calculateAddress(byte[] pkUncompressed) throws CborException { + public String calculateAddress(byte[] pkUncompressed) throws CborException, IOException { + + ByteArrayOutputStream exBaos = new ByteArrayOutputStream(); + exBaos.write(pkUncompressed); + exBaos.write(BTCUtils.fromHex("0000000000000000000000000000000000000000000000000000000000000000")); + byte[] pkExtended = exBaos.toByteArray(); + + exBaos.reset(); + new CborEncoder(exBaos).encode(new CborBuilder() + .addArray() + .add(0) + .addArray() + .add(0) + .add(pkExtended) + .end() + .addMap() + .end() + .end() + .build()); + byte[] forSha3 = exBaos.toByteArray(); + + Digest sha3 = DigestFactory.createSHA3_256(); + sha3.update(forSha3, 0, forSha3.length); + byte[] forBlake = new byte[32]; + sha3.doFinal(forBlake, 0); final Blake2b blake2b = Blake2b.Digest.newInstance(28); - byte[] pkHash = blake2b.digest(pkUncompressed); + byte[] pkHash = blake2b.digest(forBlake); //pkHash + attributes ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -461,7 +489,7 @@ public class CardanoEngine extends CoinEngine { //dataToSign prefix baos.reset(); - baos.write(new byte[] {(byte) 0x01}); + baos.write(new byte[]{(byte) 0x01}); baos.write(magic); baos.write(new byte[]{(byte) 0x58, (byte) 0x20}); @@ -469,7 +497,6 @@ public class CardanoEngine extends CoinEngine { byte[] dataToSign = baos.toByteArray(); - return new SignTask.TransactionToSign() { @Override @@ -501,11 +528,16 @@ public class CardanoEngine extends CoinEngine { @Override public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + ByteArrayOutputStream exBaos = new ByteArrayOutputStream(); + exBaos.write(ctx.getCard().getWalletPublicKey()); + exBaos.write(BTCUtils.fromHex("0000000000000000000000000000000000000000000000000000000000000000")); + byte[] pkExtended = exBaos.toByteArray(); + //pubkey + signature baos.reset(); new CborEncoder(baos).encode(new CborBuilder() .addArray() - .add(ctx.getCard().getWalletPublicKey()) + .add(pkExtended) .add(signFromCard) .end() .build()); @@ -516,23 +548,28 @@ public class CardanoEngine extends CoinEngine { //witness type + witness body baos.reset(); new CborEncoder(baos).encode(new CborBuilder() + .addArray() .addArray() .add(0) .add(witnessBodyItem) .end() + .end() .build()); byte[] witness = baos.toByteArray(); baos.reset(); - new CborEncoder(baos).encode(new CborBuilder() - .addArray() - .add(txBody) - .add(witness) - .end() - .build()); + baos.write(new byte[]{(byte) 0x82}); + baos.write(txBody); + baos.write(witness); +// new CborEncoder(baos).encode(new CborBuilder() +// .addArray() +// .add(txBody) +// .add(witness) +// .end() +// .build()); byte[] txForSend = baos.toByteArray(); notifyOnNeedSendTransaction(txForSend); - String hex = BTCUtils.toHex(txForSend); +// String hex = BTCUtils.toHex(txForSend); return txForSend; } }; @@ -613,9 +650,9 @@ public class CardanoEngine extends CoinEngine { @Override public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { - coinData.minFee = new Amount(new BigDecimal(0.2).setScale(6, RoundingMode.DOWN), ctx.getBlockchain().getCurrency()); - coinData.normalFee = new Amount(new BigDecimal(0.2).setScale(6, RoundingMode.DOWN), ctx.getBlockchain().getCurrency()); - coinData.maxFee = new Amount(new BigDecimal(0.2).setScale(6, RoundingMode.DOWN), ctx.getBlockchain().getCurrency()); + coinData.minFee = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); + coinData.normalFee = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); + coinData.maxFee = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); blockchainRequestsCallbacks.onComplete(true); } @@ -624,7 +661,7 @@ public class CardanoEngine extends CoinEngine { public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { final ServerApiAdalite serverApiAdalite = new ServerApiAdalite(); final String txStr = Base64.encodeToString(txForSend, Base64.NO_WRAP); - // BTCUtils.toHex(txForSend); + // BTCUtils.toHex(txForSend); final ServerApiAdalite.ResponseListener responseListener = new ServerApiAdalite.ResponseListener() { @Override From 3fa19dbdc9cfad9861fdc84b46f3c5316b832a87 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Feb 2019 16:35:25 +0300 Subject: [PATCH 05/11] Updated on 2026-08-14 --- .../com/tangem/data/network/AdaliteApi.java | 2 +- .../tangem/data/network/ServerApiAdalite.java | 92 +++++++++++-------- .../domain/wallet/cardano/CardanoEngine.java | 76 +++++++++------ 3 files changed, 104 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/tangem/data/network/AdaliteApi.java b/app/src/main/java/com/tangem/data/network/AdaliteApi.java index c8f84c7283..8af94a92c5 100644 --- a/app/src/main/java/com/tangem/data/network/AdaliteApi.java +++ b/app/src/main/java/com/tangem/data/network/AdaliteApi.java @@ -26,5 +26,5 @@ public interface AdaliteApi { @Headers("Content-Type: application/json") @POST(ServerApiAdalite.ADALITE_SEND) - Call adaliteSend(@Body AdaliteBody adaliteBody); + Call adaliteSend(@Body AdaliteBody adaliteBody); } diff --git a/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java b/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java index 0dd2db15c2..b057787494 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiAdalite.java @@ -6,6 +6,8 @@ import com.tangem.data.network.model.AdaliteBody; import com.tangem.data.network.model.AdaliteResponse; import com.tangem.data.network.model.AdaliteResponseUtxo; +import java.util.List; + import androidx.annotation.NonNull; import retrofit2.Call; import retrofit2.Callback; @@ -38,6 +40,8 @@ public class ServerApiAdalite { void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo); + void onSuccess(String method, List listResponse); + void onFail(String method, String message); } @@ -58,9 +62,34 @@ public class ServerApiAdalite { AdaliteApi adaliteApi = retrofitAdalite.create(AdaliteApi.class); - if (method.equals(ADALITE_UNSPENT_OUTPUTS)) { - Call call = adaliteApi.adaliteUnspent("[\"" + wallet + "\"]"); - call.enqueue(new Callback() { + switch (method) { + case ADALITE_ADDRESS: + + Call addressCall = adaliteApi.adaliteAddress(wallet); + addressCall.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()); + } + }); + break; + + case ADALITE_UNSPENT_OUTPUTS: + Call outputsCall = adaliteApi.adaliteUnspent("[\"" + wallet + "\"]"); + outputsCall.enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.code() == 200) { @@ -79,46 +108,37 @@ public class ServerApiAdalite { Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage()); } }); - } else { - Call call; - switch (method) { - case ADALITE_ADDRESS: - call = adaliteApi.adaliteAddress(wallet); - break; + break; -// case ADALITE_TRANSACTION: -// call = adaliteApi.adaliteTransaction(tx); -// break; + case ADALITE_SEND: + Call sendCall = adaliteApi.adaliteSend(new AdaliteBody(tx)); + sendCall.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()); + } + } - case ADALITE_SEND: - call = adaliteApi.adaliteSend(new AdaliteBody(tx)); - break; + @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()); + } + }); + break; default: - call = adaliteApi.adaliteAddress(wallet); + responseListener.onFail(method, "undeclared method"); + Log.e(TAG, "requestData " + method + " onFailure - undeclared method"); break; } - 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/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index a71c452502..76ec313cdc 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -38,7 +38,10 @@ import co.nstant.in.cbor.CborDecoder; import co.nstant.in.cbor.CborEncoder; import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.builder.ArrayBuilder; +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.ByteString; import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.UnsignedInteger; import static com.tangem.domain.wallet.Base58.decodeBase58; import static com.tangem.domain.wallet.Base58.encodeBase58; @@ -151,21 +154,19 @@ public class CardanoEngine extends CoinEngine { ByteArrayInputStream bais = new ByteArrayInputStream(decAddress); try { - List list = new CborDecoder(bais).decode(); -// DataItem[] array = (DataItem[]) list.toArray(); TODO: Complete -// byte[] addressDataEncoded = (byte[]) array[0].getTag().getValue(); -// int crc32Checksum = (int) array[1].getTag().getValue(); -// CRC32 Checksum = new CRC32(); -// Checksum.update(); -// if (crc32Checksum !== CRC32(addressDataEncoded)) { -// return false -// } - } catch (CborException e) { - return false; - } + List addressList = ((Array)new CborDecoder(bais).decode().get(0)).getDataItems(); + byte[] addressBytes = ((ByteString)addressList.get(0)).getBytes(); + long addressChecksum = ((UnsignedInteger)addressList.get(1)).getValue().longValue(); + final CRC32 crc32 = new CRC32(); + crc32.update(addressBytes); + long calculatedChecksum = crc32.getValue(); - if (address.length() < 30) { //TODO: Check + if (addressChecksum != calculatedChecksum) { + return false; + } + + } catch (Exception e) { return false; } @@ -545,16 +546,21 @@ public class CardanoEngine extends CoinEngine { DataItem witnessBodyItem = new CborBuilder().add(witnessBody).build().get(0); witnessBodyItem.setTag(24); + //array of witnesses + CborBuilder witnessBuilder = new CborBuilder(); + ArrayBuilder witnessArrayBuilder = witnessBuilder.addArray(); + //witness type + witness body + for (CardanoData.UnspentOutput utxo : coinData.getUnspentOutputs()) { + witnessArrayBuilder + .addArray() + .add(0) + .add(witnessBodyItem) + .end(); + } + baos.reset(); - new CborEncoder(baos).encode(new CborBuilder() - .addArray() - .addArray() - .add(0) - .add(witnessBodyItem) - .end() - .end() - .build()); + new CborEncoder(baos).encode(witnessBuilder.build()); byte[] witness = baos.toByteArray(); baos.reset(); @@ -569,7 +575,7 @@ public class CardanoEngine extends CoinEngine { // .build()); byte[] txForSend = baos.toByteArray(); notifyOnNeedSendTransaction(txForSend); -// String hex = BTCUtils.toHex(txForSend); + //String hex = BTCUtils.toHex(txForSend); return txForSend; } }; @@ -630,6 +636,12 @@ public class CardanoEngine extends CoinEngine { } } + @Override + public void onSuccess(String method, List listResponse) { + Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions"); + ctx.setError("Wrong response type for requestBalanceAndUnspentTransactions"); + } + @Override public void onFail(String method, String message) { Log.i(TAG, "onFail: " + method + " " + message); @@ -666,8 +678,20 @@ public class CardanoEngine extends CoinEngine { final ServerApiAdalite.ResponseListener responseListener = new ServerApiAdalite.ResponseListener() { @Override public void onSuccess(String method, AdaliteResponse adaliteResponse) { + Log.e(TAG, "Wrong response type for requestSendTransaction"); + ctx.setError("Wrong response type for requestSendTransaction"); + } + + @Override + public void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo) { + Log.e(TAG, "Wrong response type for requestSendTransaction"); + ctx.setError("Wrong response type for requestSendTransaction"); + } + + @Override + public void onSuccess(String method, List listResponse) { if (method.equals(ServerApiAdalite.ADALITE_SEND)) { - String resultString = adaliteResponse.toString(); + String resultString = listResponse.toString(); try { if (resultString.isEmpty()) { ctx.setError("No response from node"); @@ -689,12 +713,6 @@ public class CardanoEngine extends CoinEngine { } } - @Override - public void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo) { - Log.e(TAG, "Wrong response type for ADALITE_SEND"); - ctx.setError("Wrong response type for ADALITE_SEND"); - } - @Override public void onFail(String method, String message) { if (!serverApiAdalite.isRequestsSequenceCompleted()) { From 73be760859a18e0c3e10ddabca340ea7e6740dff Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Feb 2019 18:43:31 +0300 Subject: [PATCH 06/11] Updated on 2026-08-14 --- .../domain/wallet/cardano/CardanoEngine.java | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index 76ec313cdc..b89be36848 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -154,9 +154,9 @@ public class CardanoEngine extends CoinEngine { ByteArrayInputStream bais = new ByteArrayInputStream(decAddress); try { - List addressList = ((Array)new CborDecoder(bais).decode().get(0)).getDataItems(); - byte[] addressBytes = ((ByteString)addressList.get(0)).getBytes(); - long addressChecksum = ((UnsignedInteger)addressList.get(1)).getValue().longValue(); + List addressList = ((Array) new CborDecoder(bais).decode().get(0)).getDataItems(); + byte[] addressBytes = ((ByteString) addressList.get(0)).getBytes(); + long addressChecksum = ((UnsignedInteger) addressList.get(1)).getValue().longValue(); final CRC32 crc32 = new CRC32(); crc32.update(addressBytes); @@ -581,6 +581,25 @@ public class CardanoEngine extends CoinEngine { }; } + private int calculateEstimatedTransactionSize(String targetAddress, Amount amount) { + Amount feeDummy = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); + try { + OnNeedSendTransaction onNeedSendTransactionBackup = onNeedSendTransaction; + onNeedSendTransaction =(tx)->{}; // empty function to bypass exception + + SignTask.TransactionToSign ttsDummy = constructTransaction(amount, feeDummy, true, targetAddress); + byte[] txForSendDummy = ttsDummy.onSignCompleted(new byte[64]); + + onNeedSendTransaction = onNeedSendTransactionBackup; + Log.e(TAG, "txForSendDummy.length=" + String.valueOf(txForSendDummy.length)); + return txForSendDummy.length; + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "Can't calculate transaction size -> use default!"); + return 1000; + } + } + @Override public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { final ServerApiAdalite serverApiAdalite = new ServerApiAdalite(); @@ -662,9 +681,16 @@ public class CardanoEngine extends CoinEngine { @Override public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { - coinData.minFee = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); - coinData.normalFee = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); - coinData.maxFee = new Amount(new BigDecimal(0.2).setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); + final double A = 0.155381; + final double B = 0.000043946; + int calcSize = calculateEstimatedTransactionSize(targetAddress, amount); + + double fee = A + B * calcSize; + Amount feeAmount = new Amount(new BigDecimal(fee).setScale(getDecimals(), RoundingMode.UP), getFeeCurrency()); + + coinData.minFee = feeAmount; + coinData.normalFee = feeAmount; + coinData.maxFee = feeAmount; blockchainRequestsCallbacks.onComplete(true); } @@ -725,4 +751,9 @@ public class CardanoEngine extends CoinEngine { serverApiAdalite.requestData(ServerApiAdalite.ADALITE_SEND, "", txStr); } + + @Override + public boolean allowSelectFeeLevel() { + return false; + } } From 672101edc28ab562a74c0fafad5ad6bf375aa760 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Feb 2019 14:14:04 +0300 Subject: [PATCH 07/11] Updated on 2026-08-14 --- .../src/main/res/drawable/card_ff32.png | Bin 0 -> 36831 bytes .../tangemserver/android/data/LocalStorage.kt | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 tangemcard-android/src/main/res/drawable/card_ff32.png diff --git a/tangemcard-android/src/main/res/drawable/card_ff32.png b/tangemcard-android/src/main/res/drawable/card_ff32.png new file mode 100644 index 0000000000000000000000000000000000000000..3e0cd619953ff9287d032fbc983f2b87d5ad3256 GIT binary patch literal 36831 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 literal 0 HcmV?d00001 diff --git a/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt b/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt index 707e189b87..2b8e6204ba 100644 --- a/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt +++ b/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt @@ -70,7 +70,8 @@ class LocalStorage putResourceArtworkToCatalog(R.drawable.card_ru029, false) putResourceArtworkToCatalog(R.drawable.card_ru030, false) putResourceArtworkToCatalog(R.drawable.card_ru031, false) - putResourceArtworkToCatalog(R.drawable.card_ru032, true) + putResourceArtworkToCatalog(R.drawable.card_ru032, false) + putResourceArtworkToCatalog(R.drawable.card_ff32, true) } if (batchesFile.exists()) { try { @@ -243,6 +244,7 @@ class LocalStorage card.batch == "0020" -> R.drawable.card_ru030 card.batch == "0021" -> R.drawable.card_ru031 card.batch == "0022" -> R.drawable.card_ru032 + card.batch == "FF32" -> R.drawable.card_ff32 else -> null } From 77b304c1fdf43cff7aef9cb69613169474b596bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Feb 2019 15:57:12 +0300 Subject: [PATCH 08/11] Updated on 2026-08-14 --- .idea/misc.xml | 35 ++++++++++++++++++ .../domain/wallet/cardano/CardanoEngine.java | 8 ++-- .../tangemserver/android/data/LocalStorage.kt | 2 +- .../src/main/res/drawable/card_ff32.png | Bin 0 -> 36831 bytes 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 tangemserver-android/src/main/res/drawable/card_ff32.png diff --git a/.idea/misc.xml b/.idea/misc.xml index 703e5d4b89..cd2f3a654d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,6 +5,41 @@ + + + + diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index b89be36848..8561f4c0ff 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -15,10 +15,10 @@ import com.tangem.domain.wallet.Base58; import com.tangem.domain.wallet.CoinData; import com.tangem.domain.wallet.CoinEngine; import com.tangem.domain.wallet.TangemContext; -import com.tangem.tangemcard.data.TangemCard; -import com.tangem.tangemcard.reader.CardProtocol; -import com.tangem.tangemcard.tasks.SignTask; -import com.tangem.tangemcard.util.Util; +import com.tangem.tangemcommon.data.TangemCard; +import com.tangem.tangemcommon.reader.CardProtocol; +import com.tangem.tangemcommon.tasks.SignTask; +import com.tangem.tangemcommon.util.Util; import com.tangem.util.DecimalDigitsInputFilter; import com.tangem.wallet.R; diff --git a/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt b/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt index 7d4d6c3a10..5f5ce53f51 100644 --- a/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt +++ b/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt @@ -49,7 +49,7 @@ class LocalStorage artworks = HashMap() } - if (artworks.count() == 0) { + if (artworks.count() < 21) { // forceSave=true only on the last one putResourceArtworkToCatalog(R.drawable.card_default, false) putResourceArtworkToCatalog(R.drawable.card_default_nft, false) diff --git a/tangemserver-android/src/main/res/drawable/card_ff32.png b/tangemserver-android/src/main/res/drawable/card_ff32.png new file mode 100644 index 0000000000000000000000000000000000000000..3e0cd619953ff9287d032fbc983f2b87d5ad3256 GIT binary patch literal 36831 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 literal 0 HcmV?d00001 From 2ac7c0dc2b5c883e5fa91b711f5d2331ed1c0ff6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Feb 2019 16:19:06 +0300 Subject: [PATCH 09/11] Updated on 2026-08-14 --- .../presentation/activity/ConfirmTransactionActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt b/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt index 92815f8889..2e62f6dd2f 100644 --- a/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt +++ b/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt @@ -88,10 +88,14 @@ class ConfirmTransactionActivity : AppCompatActivity(), NfcAdapter.ReaderCallbac btnSend.visibility = View.INVISIBLE val allowFeeLevelSelection = engine.allowSelectFeeLevel() - for (lol in rgFee.touchables) { + for (lol in rgFee.touchables) {//TODO: leave invisible only? lol.isEnabled = allowFeeLevelSelection } + if (!allowFeeLevelSelection) { + rgFee.visibility = View.INVISIBLE + } + // set listeners rgFee.setOnCheckedChangeListener { _, checkedId -> doSetFee(checkedId) } etFee.addTextChangedListener(object : TextWatcher { From 74186409940ab03f9b7965b9c4db6db7e7260cf9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Feb 2019 12:03:06 +0300 Subject: [PATCH 10/11] Updated on 2026-08-14 --- app/src/main/java/com/tangem/domain/wallet/CoinData.java | 2 +- app/src/main/java/com/tangem/domain/wallet/CoinEngine.java | 1 - .../main/java/com/tangem/domain/wallet/eth/EthEngine.java | 4 ++-- .../java/com/tangem/domain/wallet/token/TokenEngine.java | 4 ++-- .../presentation/activity/ConfirmTransactionActivity.kt | 7 +------ .../java/com/tangem/presentation/fragment/LoadedWallet.kt | 2 +- app/src/main/res/layout/activity_confirm_transaction.xml | 4 ++-- 7 files changed, 9 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinData.java b/app/src/main/java/com/tangem/domain/wallet/CoinData.java index 9447ee6935..9ec1663bdb 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinData.java +++ b/app/src/main/java/com/tangem/domain/wallet/CoinData.java @@ -143,7 +143,7 @@ public abstract class CoinData { public void clearInfo() { setIsBalanceEqual(false); - setBalanceReceived(false); // TODO check + setBalanceReceived(false); setValidationNodeDescription(""); minFee=null; maxFee=null; diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java index 5bfb7b9b4e..e4cfb67533 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java @@ -192,7 +192,6 @@ public abstract class CoinEngine { public abstract boolean checkNewTransactionAmount(Amount amount); - // TODO - move minFeeInInternalUnits to CoinData public abstract boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded); public abstract boolean validateBalance(BalanceValidator balanceValidator); diff --git a/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java b/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java index 0f0086f8d1..7ddebba10a 100644 --- a/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java @@ -321,8 +321,8 @@ public class EthEngine extends CoinEngine { if (!coinData.getUnconfirmedTXCount().equals(coinData.getConfirmedTXCount())) { balanceValidator.setScore(0); - balanceValidator.setFirstLine("Unguaranteed balance"); - balanceValidator.setSecondLine("Transaction is in progress. Wait for confirmation in blockchain."); + balanceValidator.setFirstLine("Transaction in progress"); + balanceValidator.setSecondLine("Wait for confirmation in blockchain"); return false; } diff --git a/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java b/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java index 96d178234f..ee10a59073 100644 --- a/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java @@ -394,8 +394,8 @@ public class TokenEngine extends CoinEngine { if (!coinData.getUnconfirmedTXCount().equals(coinData.getConfirmedTXCount())) { balanceValidator.setScore(0); - balanceValidator.setFirstLine("Unguaranteed balance"); - balanceValidator.setSecondLine("Transaction is in progress. Wait for confirmation in blockchain."); + balanceValidator.setFirstLine("Transaction in progress"); + balanceValidator.setSecondLine("Wait for confirmation in blockchain"); return false; } diff --git a/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt b/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt index 2e62f6dd2f..adede77949 100644 --- a/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt +++ b/app/src/main/java/com/tangem/presentation/activity/ConfirmTransactionActivity.kt @@ -87,12 +87,7 @@ class ConfirmTransactionActivity : AppCompatActivity(), NfcAdapter.ReaderCallbac btnSend.visibility = View.INVISIBLE - val allowFeeLevelSelection = engine.allowSelectFeeLevel() - for (lol in rgFee.touchables) {//TODO: leave invisible only? - lol.isEnabled = allowFeeLevelSelection - } - - if (!allowFeeLevelSelection) { + if (!engine.allowSelectFeeLevel()) { rgFee.visibility = View.INVISIBLE } diff --git a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt index 6e3b09fbb1..9a25817c4d 100644 --- a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt +++ b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt @@ -729,7 +729,7 @@ class LoadedWallet : androidx.fragment.app.Fragment(), NfcAdapter.ReaderCallback Blockchain.Litecoin -> "litecoin" Blockchain.Rootstock -> "bitcoin" Blockchain.RootstockToken -> "bitcoin" - Blockchain.Cardano -> "cardano" //TODO:check + Blockchain.Cardano -> "cardano" else -> { throw Exception("Can''t get rate for blockchain " + ctx.blockchainName) } diff --git a/app/src/main/res/layout/activity_confirm_transaction.xml b/app/src/main/res/layout/activity_confirm_transaction.xml index acb261225c..decf945d60 100644 --- a/app/src/main/res/layout/activity_confirm_transaction.xml +++ b/app/src/main/res/layout/activity_confirm_transaction.xml @@ -263,7 +263,7 @@ + app:layout_constraintWidth_min="130dp" /> Date: Wed, 27 Feb 2019 16:31:28 +0300 Subject: [PATCH 11/11] Updated on 2026-08-14 --- .../domain/wallet/bch/BitcoinCashNode.kt | 7 +- .../tangem/domain/wallet/btc/BitcoinNode.kt | 67 ++++++++----------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt b/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt index 7123f17dd2..66eedee951 100644 --- a/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt @@ -8,8 +8,7 @@ enum class BitcoinCashNode(val host: String, val port: Int, val proto: String) { N_005("bch.crypto.mldlabs.com", 50002, "ssl"), N_006("electron.coinucopia.io", 50002, "ssl"), N_007("blackie.c3-soft.com", 50002, "ssl"), - N_008("electroncash.ueo.ch", 51002, "ssl"), - N_009("electrum.imaginary.cash", 50002, "ssl"), - N_010("bitcoincash.quangld.com", 50002, "ssl"), - N_011("bch.stitthappens.com", 50002, "ssl"), + N_008("electrum.imaginary.cash", 50002, "ssl"), + N_009("bitcoincash.quangld.com", 50002, "ssl"), + N_010("bch.stitthappens.com", 50002, "ssl"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt b/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt index 6a0c66d540..2af5648db2 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt @@ -2,44 +2,31 @@ package com.tangem.domain.wallet.btc enum class BitcoinNode(val host: String, val port: Int, val proto: String) { N_001("electrum.anduck.net", 50001, "tcp"), - N_002("ip119.ip-54-37-91.eu", 50001, "tcp"), - N_003("electrum.qtornado.com", 50001, "tcp"), - N_004("ip239.ip-54-36-234.eu", 50001, "tcp"), - N_005("electrum-server.ninja", 50001, "tcp"), - N_006("vps.hsmiths.com", 8080, "tcp"), - N_007("ip120.ip-54-37-91.eu", 50001, "tcp"), - N_008("ip101.ip-54-37-91.eu", 50001, "tcp"), - N_009("e-x.not.fyi", 50001, "tcp"), - N_010("electrum.vom-stausee.de", 50001, "tcp"), - N_011("electrum2.eff.ro", 50001, "tcp"), - N_012("electrum.coinucopia.io", 50001, "tcp"), - N_013("b.ooze.cc", 50002, "ssl"), - N_014("electrum.nute.net", 50002, "ssl"), - N_015("electrum.coinop.cc", 50002, "ssl"), - N_016("electrum.vom-stausee.de", 50002, "ssl"), - N_017("ip119.ip-54-37-91.eu", 50002, "ssl"), - N_018("ip101.ip-54-37-91.eu", 50002, "ssl"), - N_019("dedi.jochen-hoenicke.de", 50002, "ssl"), - N_020("ip239.ip-54-36-234.eu", 50002, "ssl"), - N_021("e-x.not.fyi", 50002, "ssl"), - N_022("ip120.ip-54-37-91.eu", 50002, "ssl"), - N_023("electrum.villocq.com", 50002, "ssl"), - N_024("electrum.anduck.net", 50012, "ssl"), - N_025("technetium.network", 50002, "ssl"), - N_026("electrum.coinucopia.io", 50002, "ssl"), - N_027("dimon.trimon.de", 50002, "ssl"), - N_028("btc.gravitech.net", 50002, "ssl"), - N_029("hetzner01.fischl-online.de", 50002, "ssl"), - N_030("fn.48.org", 50002, "ssl"), - N_031("185.64.116.15", 50002, "ssl"), - N_032("vps.hsmiths.com", 50002, "ssl"), - N_033("104.250.141.242", 50002, "ssl"), - N_034("electrum.qtornado.com", 50002, "ssl"), - N_035("electrum-server.ninja", 50002, "ssl"), - N_036("electrum2.eff.ro", 50002, "ssl"), - N_037("electrum.hsmiths.com", 995, "ssl"), - N_038("electrum.hsmiths.com", 50002, "ssl"), - N_039("139.162.14.142", 50002, "ssl"), - N_040("tardis.bauerj.eu", 50002, "ssl"), - N_041("such.ninja", 50002, "ssl"), + N_002("electrum.qtornado.com", 50001, "tcp"), + N_003("e-x.not.fyi", 50001, "tcp"), + N_004("electrum.vom-stausee.de", 50001, "tcp"), + N_005("electrum2.eff.ro", 50001, "tcp"), + N_006("electrum.coinucopia.io", 50001, "tcp"), + N_007("electrum.nute.net", 50002, "ssl"), + N_008("electrum.coinop.cc", 50002, "ssl"), + N_009("electrum.vom-stausee.de", 50002, "ssl"), + N_010("dedi.jochen-hoenicke.de", 50002, "ssl"), + N_011("e-x.not.fyi", 50002, "ssl"), + N_012("ip120.ip-54-37-91.eu", 50002, "ssl"), + N_013("electrum.villocq.com", 50002, "ssl"), + N_014("electrum.anduck.net", 50012, "ssl"), + N_015("technetium.network", 50002, "ssl"), + N_016("electrum.coinucopia.io", 50002, "ssl"), + N_017("dimon.trimon.de", 50002, "ssl"), + N_018("btc.gravitech.net", 50002, "ssl"), + N_019("hetzner01.fischl-online.de", 50002, "ssl"), + N_020("fn.48.org", 50002, "ssl"), + N_021("vps.hsmiths.com", 50002, "ssl"), + N_022("electrum.qtornado.com", 50002, "ssl"), + N_023("electrum-server.ninja", 50002, "ssl"), + N_024("electrum2.eff.ro", 50002, "ssl"), + N_025("electrum.hsmiths.com", 995, "ssl"), + N_026("electrum.hsmiths.com", 50002, "ssl"), + N_027("139.162.14.142", 50002, "ssl"), + N_028("tardis.bauerj.eu", 50002, "ssl"), } \ No newline at end of file