Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-03 13:13:56 +03:00
commit 4d10bc64e9
99 changed files with 6022 additions and 602 deletions

195
.idea/navEditor.xml generated
View file

@ -8,13 +8,25 @@
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="cameraFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-397" />
<option name="y" value="645" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="confirmTransactionFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="516" />
<option name="y" value="408" />
<option name="x" value="744" />
<option name="y" value="2390" />
</Point>
</option>
<option name="myPositions">
@ -39,8 +51,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="324" />
<option name="y" value="-682" />
<option name="x" value="500" />
<option name="y" value="105" />
</Point>
</option>
</LayoutPositions>
@ -51,8 +63,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="56" />
<option name="y" value="-379" />
<option name="x" value="256" />
<option name="y" value="105" />
</Point>
</option>
<option name="myPositions">
@ -77,13 +89,65 @@
</LayoutPositions>
</value>
</entry>
<entry key="idFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-272" />
<option name="y" value="1404" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_idFragment_to_issueNewIdFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_idFragment_to_verifyCard">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="issueNewIdFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-394" />
<option name="y" value="1004" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_issueNewIdFragment_to_cameraFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_issueNewIdFragment_to_validateIdFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="loadedWalletFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-91" />
<option name="y" value="411" />
<option name="x" value="256" />
<option name="y" value="1279" />
</Point>
</option>
<option name="myPositions">
@ -128,8 +192,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-642" />
<option name="y" value="5" />
<option name="x" value="256" />
<option name="y" value="2390" />
</Point>
</option>
<option name="myPositions">
@ -154,8 +218,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-425" />
<option name="y" value="250" />
<option name="x" value="12" />
<option name="y" value="1279" />
</Point>
</option>
<option name="myPositions">
@ -165,6 +229,11 @@
<LayoutPositions />
</value>
</entry>
<entry key="action_main_to_idFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_main_to_loadedWalletFragment">
<value>
<LayoutPositions />
@ -190,6 +259,11 @@
<LayoutPositions />
</value>
</entry>
<entry key="action_main_to_tagFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
@ -200,8 +274,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="416" />
<option name="y" value="-239" />
<option name="x" value="988" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
@ -212,8 +286,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-274" />
<option name="y" value="-387" />
<option name="x" value="256" />
<option name="y" value="461" />
</Point>
</option>
</LayoutPositions>
@ -224,8 +298,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="76" />
<option name="y" value="811" />
<option name="x" value="744" />
<option name="y" value="1039" />
</Point>
</option>
</LayoutPositions>
@ -236,8 +310,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="280" />
<option name="y" value="1172" />
<option name="x" value="500" />
<option name="y" value="1992" />
</Point>
</option>
<option name="myPositions">
@ -257,8 +331,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="9" />
<option name="y" value="1151" />
<option name="x" value="500" />
<option name="y" value="1618" />
</Point>
</option>
</LayoutPositions>
@ -269,8 +343,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-214" />
<option name="y" value="1097" />
<option name="x" value="500" />
<option name="y" value="1261" />
</Point>
</option>
</LayoutPositions>
@ -281,8 +355,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="411" />
<option name="x" value="500" />
<option name="y" value="2348" />
</Point>
</option>
<option name="myPositions">
@ -307,8 +381,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="941" />
<option name="y" value="15" />
<option name="x" value="744" />
<option name="y" value="682" />
</Point>
</option>
<option name="myPositions">
@ -328,8 +402,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="328" />
<option name="y" value="807" />
<option name="x" value="744" />
<option name="y" value="2034" />
</Point>
</option>
</LayoutPositions>
@ -340,8 +414,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="1011" />
<option name="y" value="412" />
<option name="x" value="1232" />
<option name="y" value="2433" />
</Point>
</option>
</LayoutPositions>
@ -352,8 +426,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-496" />
<option name="y" value="-375" />
<option name="x" value="256" />
<option name="y" value="923" />
</Point>
</option>
</LayoutPositions>
@ -364,8 +438,8 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="760" />
<option name="y" value="412" />
<option name="x" value="988" />
<option name="y" value="2433" />
</Point>
</option>
<option name="myPositions">
@ -380,13 +454,46 @@
</LayoutPositions>
</value>
</entry>
<entry key="tagFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="1635" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="validateIdFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-628" />
<option name="y" value="1001" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_validateIdFragment_to_writeIdFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="verifyCard">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="78" />
<option name="y" value="32" />
<option name="x" value="500" />
<option name="y" value="860" />
</Point>
</option>
<option name="myPositions">
@ -411,6 +518,18 @@
</LayoutPositions>
</value>
</entry>
<entry key="writeIdFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-661" />
<option name="y" value="701" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>

View file

@ -164,4 +164,7 @@ dependencies {
implementation 'io.jafka:jeos:0.9.15'
// implementation 'one.block:eosiojava:0.1.0'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.61'
implementation 'io.fotoapparat:fotoapparat:2.6.1'
implementation 'com.otaliastudios:cameraview:2.6.1'
}

View file

@ -10,6 +10,7 @@ public enum Blockchain {
Bitcoin("BTC", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Bitcoin"),
BitcoinTestNet("BTC/test", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin_testnet, "Bitcoin Testnet"),
Ethereum("ETH", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"),
EthereumId("ETH/ID", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum ID"),
EthereumTestNet("ETH/test", "ETH", 1.0, R.drawable.ic_logo_ethereum_testnet, "Ethereum Testnet"),
Token("Token", "ETH", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"),
NftToken("NftToken", "", 1.0, R.drawable.tangem2, "Ethereum"),
@ -23,10 +24,13 @@ public enum Blockchain {
BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance Testnet"),
Matic("MATIC", "MTX", 1.0, R.drawable.tangem2, "Matic"),
MaticTestNet("MATIC/test", "MTX", 1.0, R.drawable.tangem2, "Matic Testnet"),
Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"),
StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"),
Stellar("XLM", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"),
StellarTestNet("XLM/test", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"),
StellarAsset("Asset", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"),
Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS");
StellarTag("XLM-Tag", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"),
Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"),
Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"),
Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.tangem2, "Tezos");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;

View file

@ -15,13 +15,13 @@ import retrofit2.http.Query;
public interface BlockcypherApi {
@GET(Server.ApiBlockcypher.Method.MAIN)
Call<BlockcypherFee> blockcypherMain(@Path("blockchain") String blockchain, @Path("network") String network);
Call<BlockcypherFee> blockcypherMain(@Path("blockchain") String blockchain, @Path("network") String network, @Query("token") String token);
@GET(Server.ApiBlockcypher.Method.ADDRESS)
Call<BlockcypherResponse> blockcypherAddress(@Path("blockchain") String blockchain, @Path("network") String network, @Path("address") String address);
Call<BlockcypherResponse> blockcypherAddress(@Path("blockchain") String blockchain, @Path("network") String network, @Path("address") String address, @Query("token") String token);
@GET(Server.ApiBlockcypher.Method.TXS)
Call<BlockcypherTx> blockcypherTxs(@Path("blockchain") String blockchain, @Path("network") String network, @Path("txHash") String txHash);
Call<BlockcypherTx> blockcypherTxs(@Path("blockchain") String blockchain, @Path("network") String network, @Path("txHash") String txHash, @Query("token") String token);
@Headers("Content-Type: application/json")
@POST(Server.ApiBlockcypher.Method.PUSH)

View file

@ -0,0 +1,25 @@
package com.tangem.data.network;
import com.tangem.data.network.model.BitcoreBalance;
import com.tangem.data.network.model.BitcoreSendBody;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
import java.util.List;
import io.reactivex.Single;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
public interface DucatusApi {
@GET(Server.ApiDucatus.Method.BALANCE)
Single<BitcoreBalance> ducatusBalance(@Path("address") String address);
@GET(Server.ApiDucatus.Method.UTXO)
Single<List<BitcoreUtxo>> ducatusUnspents(@Path("address") String address);
@POST(Server.ApiDucatus.Method.SEND)
Single<BitcoreSendResponse> ducatusSend(@Body BitcoreSendBody body);
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.network;
import com.tangem.data.network.model.InsightBody;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import java.util.List;
@ -18,7 +19,7 @@ public interface InsightApi {
Call<InsightResponse> insightAddress(@Path("address") String address);
@GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS)
Call<List<InsightResponse>> insightUnspent(@Path("address") String address);
Call<List<InsightUtxo>> insightUnspent(@Path("address") String address);
@GET(ServerApiInsight.INSIGHT_TRANSACTION)
Call<InsightResponse> insightTransaction(@Path("txId") String txId);

View file

@ -129,4 +129,14 @@ public class Server {
static final String PUSH = URL_BLOCKCHAININFO + "pushtx";
}
}
public static class ApiDucatus {
public static final String URL_DUCATUS = ServerURL.API_DUCATUS + "api/DUC/mainnet/";
public static class Method {
static final String BALANCE = URL_DUCATUS + "address/{address}/balance";
static final String UTXO = URL_DUCATUS + "address/{address}/?unspent=true";
static final String SEND = URL_DUCATUS + "tx/send";
}
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.data.network;
import com.tangem.App;
import com.tangem.data.network.model.BitcoreBalance;
import com.tangem.data.network.model.BitcoreBalanceAndUnspents;
import com.tangem.data.network.model.BitcoreSendBody;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
import com.tangem.tangem_card.util.Log;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class ServerApiBitcore {
private static String TAG = ServerApiBitcore.class.getSimpleName();
public void getBalanceAndUnspents(String wallet, SingleObserver<BitcoreBalanceAndUnspents> balanceAndUnspentsObserver) {
Log.i(TAG, "new getAddressAndUnspents request");
DucatusApi api = App.Companion.getNetworkComponent().getRetrofitDucatus().create(DucatusApi.class);
Single<BitcoreBalance> balanceObservable = api.ducatusBalance(wallet);
Single<List<BitcoreUtxo>> unspentsObservable = api.ducatusUnspents(wallet)
.onErrorReturnItem(new ArrayList<>());
Single.zip(balanceObservable, unspentsObservable, BitcoreBalanceAndUnspents::new)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(balanceAndUnspentsObserver);
}
public void sendTransaction(String tx, SingleObserver<BitcoreSendResponse> sendObserver) {
Log.i(TAG, "new getAddress request");
DucatusApi api = App.Companion.getNetworkComponent().getRetrofitDucatus().create(DucatusApi.class);
Single<BitcoreSendResponse> sendObservable = api.ducatusSend(new BitcoreSendBody(tx))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
sendObservable.subscribe(sendObserver);
}
}

View file

@ -32,8 +32,9 @@ public class ServerApiBlockcypher {
return requestsCount <= 0;
}
private ResponseListener responseListener;
private String apiKey = null;
private ResponseListener responseListener;
private TxResponseListener txResponseListener;
public interface ResponseListener {
@ -71,17 +72,24 @@ public class ServerApiBlockcypher {
switch (method) {
case BLOCKCYPHER_ADDRESS:
Call<BlockcypherResponse> addressCall = blockcypherApi.blockcypherAddress(blockchain, network, wallet);
Call<BlockcypherResponse> addressCall = blockcypherApi.blockcypherAddress(blockchain, network, wallet, apiKey);
addressCall.enqueue(new Callback<BlockcypherResponse>() {
@Override
public void onResponse(@NonNull Call<BlockcypherResponse> call, @NonNull Response<BlockcypherResponse> response) {
requestsCount--;
if (response.code() == 200) {
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());
switch (response.code()) {
case 200:
responseListener.onSuccess(method, response.body());
Log.i(TAG, "requestData " + method + " onResponse " + response.code());
break;
case 429:
apiKey = getRandomApiKey();
requestData(blockchainID, method, wallet, tx);
break;
default:
responseListener.onFail(method, String.valueOf(response.code()));
Log.e(TAG, "requestData " + method + " onResponse " + response.code());
break;
}
}
@ -95,17 +103,24 @@ public class ServerApiBlockcypher {
break;
case BLOCKCYPHER_FEE:
Call<BlockcypherFee> feeCall = blockcypherApi.blockcypherMain(blockchain, network);
Call<BlockcypherFee> feeCall = blockcypherApi.blockcypherMain(blockchain, network, apiKey);
feeCall.enqueue(new Callback<BlockcypherFee>() {
@Override
public void onResponse(@NonNull Call<BlockcypherFee> call, @NonNull Response<BlockcypherFee> response) {
requestsCount--;
if (response.code() == 200) {
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());
switch (response.code()) {
case 200:
responseListener.onSuccess(method, response.body());
Log.i(TAG, "requestData " + method + " onResponse " + response.code());
break;
case 429:
apiKey = getRandomApiKey();
requestData(blockchainID, method, wallet, tx);
break;
default:
responseListener.onFail(method, String.valueOf(response.code()));
Log.e(TAG, "requestData " + method + " onResponse " + response.code());
break;
}
}
@ -119,17 +134,24 @@ public class ServerApiBlockcypher {
break;
case BLOCKCYPHER_TXS:
Call<BlockcypherTx> txsCall = blockcypherApi.blockcypherTxs(blockchain, network, tx);
Call<BlockcypherTx> txsCall = blockcypherApi.blockcypherTxs(blockchain, network, tx, apiKey);
txsCall.enqueue(new Callback<BlockcypherTx>() {
@Override
public void onResponse(@NonNull Call<BlockcypherTx> call,@NonNull Response<BlockcypherTx> response) {
requestsCount--;
if (response.code() == 200) {
txResponseListener.onSuccess(response.body());
Log.i(TAG, "requestData " + method + " onResponse " + response.code());
} else {
txResponseListener.onFail(String.valueOf(response.code()));
Log.e(TAG, "requestData " + method + " onResponse " + response.code());
switch (response.code()) {
case 200:
txResponseListener.onSuccess(response.body());
Log.i(TAG, "requestData " + method + " onResponse " + response.code());
break;
case 429:
apiKey = getRandomApiKey();
requestData(blockchainID, method, wallet, tx);
break;
default:
txResponseListener.onFail(String.valueOf(response.code()));
Log.e(TAG, "requestData " + method + " onResponse " + response.code());
break;
}
}
@ -143,19 +165,24 @@ public class ServerApiBlockcypher {
break;
case BLOCKCYPHER_SEND:
BlockcypherToken blockcypherToken = BlockcypherToken.values()[new Random().nextInt(BlockcypherToken.values().length)];
Call<BlockcypherResponse> sendCall = blockcypherApi.blockcypherPush(blockchain, network, new BlockcypherBody(tx), blockcypherToken.getToken());
Call<BlockcypherResponse> sendCall = blockcypherApi.blockcypherPush(blockchain, network, new BlockcypherBody(tx), apiKey);
sendCall.enqueue(new Callback<BlockcypherResponse>() {
@Override
public void onResponse(@NonNull Call<BlockcypherResponse> call, @NonNull Response<BlockcypherResponse> response) {
requestsCount--;
if (response.code() == 201) {
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());
switch (response.code()) {
case 201:
responseListener.onSuccess(method, response.body());
Log.i(TAG, "requestData " + method + " onResponse " + response.code());
break;
case 429:
apiKey = getRandomApiKey();
requestData(blockchainID, method, wallet, tx);
break;
default:
responseListener.onFail(method, String.valueOf(response.code()));
Log.e(TAG, "requestData " + method + " onResponse " + response.code());
break;
}
}
@ -175,4 +202,9 @@ public class ServerApiBlockcypher {
break;
}
}
private String getRandomApiKey() {
return BlockcypherToken
.values()[new Random().nextInt(BlockcypherToken.values().length)].getToken();
}
}

View file

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import com.tangem.data.network.model.InsightBody;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import java.util.List;
@ -18,11 +19,11 @@ import retrofit2.converter.gson.GsonConverterFactory;
public class ServerApiInsight {
private static String TAG = ServerApiInsight.class.getSimpleName();
public static final String INSIGHT_ADDRESS = "/addr/{address}";
public static final String INSIGHT_UNSPENT_OUTPUTS = "/addr/{address}/utxo";
public static final String INSIGHT_TRANSACTION = "/rawtx/{txId}";
public static final String INSIGHT_FEE = "/utils/estimatefee?nbBlocks=2,3,6";
public static final String INSIGHT_SEND = "/tx/send";
public static final String INSIGHT_ADDRESS = "addr/{address}";
public static final String INSIGHT_UNSPENT_OUTPUTS = "addr/{address}/utxo";
public static final String INSIGHT_TRANSACTION = "rawtx/{txId}";
public static final String INSIGHT_FEE = "utils/estimatefee?nbBlocks=2,3,6";
public static final String INSIGHT_SEND = "tx/send";
private int requestsCount = 0;
@ -38,7 +39,7 @@ public class ServerApiInsight {
public interface ResponseListener {
void onSuccess(String method, InsightResponse insightResponse);
void onSuccess(String method, List<InsightResponse> utxoList);
void onSuccess(String method, List<InsightUtxo> utxoList);
void onFail(String method, String message);
}
@ -49,7 +50,7 @@ public class ServerApiInsight {
public void requestData(String method, String wallet, String tx) {
requestsCount++;
String insightURL = "http://130.185.109.17:3001/insigth-api"; //TODO: make random selection
String insightURL = "https://insight.ducatus.io/insight-lite-api/"; //TODO: make random selection
this.lastNode = insightURL; //TODO: show node instead of URL
Retrofit retrofitInsight = new Retrofit.Builder()
@ -61,10 +62,10 @@ public class ServerApiInsight {
InsightApi insightApi = retrofitInsight.create(InsightApi.class);
if (method.equals(INSIGHT_UNSPENT_OUTPUTS)) {
Call<List<InsightResponse>> call = insightApi.insightUnspent(wallet);
call.enqueue(new Callback<List<InsightResponse>>() {
Call<List<InsightUtxo>> call = insightApi.insightUnspent(wallet);
call.enqueue(new Callback<List<InsightUtxo>>() {
@Override
public void onResponse(@NonNull Call<List<InsightResponse>> call, @NonNull Response<List<InsightResponse>> response) {
public void onResponse(@NonNull Call<List<InsightUtxo>> call, @NonNull Response<List<InsightUtxo>> response) {
requestsCount--;
if (response.code() == 200) {
@ -77,7 +78,7 @@ public class ServerApiInsight {
}
@Override
public void onFailure(@NonNull Call<List<InsightResponse>> call, @NonNull Throwable t) {
public void onFailure(@NonNull Call<List<InsightUtxo>> call, @NonNull Throwable t) {
requestsCount--;
responseListener.onFail(method, String.valueOf(t.getMessage()));
Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage());
@ -92,13 +93,9 @@ public class ServerApiInsight {
call = insightApi.insightAddress(wallet);
break;
case INSIGHT_TRANSACTION:
call = insightApi.insightTransaction(tx);
break;
case INSIGHT_FEE:
call = insightApi.insightFee();
break;
// case INSIGHT_FEE:
// call = insightApi.insightFee();
// break;
case INSIGHT_SEND:
call = insightApi.insightSend(new InsightBody(tx));

View file

@ -33,7 +33,7 @@ import io.reactivex.schedulers.Schedulers;
public class ServerApiStellar {
public ServerApiStellar(Blockchain blockchain) {
if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset) {
if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) {
currentURL = ServerURL.API_STELLAR;
} else {
currentURL = ServerURL.API_STELLAR_TESTNET;
@ -173,10 +173,11 @@ public class ServerApiStellar {
stellarRequest.setError(null);
try {
Server server;
if (ctx.getBlockchain() == Blockchain.Stellar || ctx.getBlockchain() == Blockchain.StellarAsset) {
Blockchain blockchain = ctx.getBlockchain();
if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) {
Network.usePublicNetwork();
server = new Server(currentURL);
} else if (ctx.getBlockchain() == Blockchain.StellarTestNet) {
} else if (blockchain == Blockchain.StellarTestNet) {
Network.useTestNetwork();
server = new Server(currentURL);
} else {

View file

@ -0,0 +1,139 @@
package com.tangem.data.network;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import com.tangem.data.network.model.TezosAccountResponse;
import com.tangem.data.network.model.TezosForgeBody;
import com.tangem.data.network.model.TezosHeaderResponse;
import com.tangem.data.network.model.TezosPreapplyBody;
import com.tangem.tangem_card.util.Log;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class ServerApiTezos {
private static String TAG = ServerApiTezos.class.getSimpleName();
private final String letzbakeURI = "https://teznode.letzbake.com";
private final String tezrpcURI = "https://mainnet.tezrpc.me";
static final String TEZOS_ADDRESS = "chains/main/blocks/head/context/contracts/{address}";
static final String TEZOS_HEADER = "chains/main/blocks/head/header";
static final String TEZOS_MANAGER_KEY = "chains/main/blocks/head/context/contracts/{address}/manager_key";
static final String TEZOS_FORGE_OPERATIONS = "chains/main/blocks/head/helpers/forge/operations";
static final String TEZOS_PREAPPLY_OPERATIONS = "chains/main/blocks/head/helpers/preapply/operations";
static final String TEZOS_RUN_OPERATION = "chains/main/blocks/head/helpers/scripts/run_operation";
static final String TEZOS_INJECT_OPERATIONS = "injection/operation";
private Retrofit retrofitTezos = new Retrofit.Builder()
.baseUrl(letzbakeURI)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
//logging for testing
.client(new OkHttpClient.Builder().addInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
).build())
.build();
private TezosApi tezosApi = retrofitTezos.create(TezosApi.class);
private int requestsCount = 0;
public boolean isRequestsSequenceCompleted() {
Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount));
return requestsCount <= 0;
}
public void getAddress(String wallet, SingleObserver<TezosAccountResponse> accountObserver) {
requestsCount++;
Log.i(TAG, "new getAddress request");
Single<TezosAccountResponse> accountSingle = tezosApi.getAccount(wallet)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
accountSingle.subscribe(accountObserver);
}
public void getMangerKey(String wallet, SingleObserver<String> accountObserver) {
requestsCount++;
Log.i(TAG, "new getManagerKey request");
Single<String> managerKeySingle = tezosApi.getManagerKey(wallet)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
managerKeySingle.subscribe(accountObserver);
}
public TezosHeaderResponse getHeader() throws Exception { // TODO? not async
requestsCount++;
Log.i(TAG, "new getHeader request");
Response<TezosHeaderResponse> headerResponse = tezosApi.getHeader().execute();
requestsCount--;
if (headerResponse.code() == 200) {
return headerResponse.body();
} else {
throw new Exception("Wrong header response, code: " + headerResponse.code());
}
}
public String forgeOperations(TezosForgeBody tezosForgeBody) throws Exception { // TODO? not async
requestsCount++;
Log.i(TAG, "new forgeOperations request");
Response<String> forgeResponse = tezosApi.forgeOperations(tezosForgeBody).execute();
requestsCount--;
if (forgeResponse.code() == 200) {
return forgeResponse.body();
} else {
throw new Exception("Wrong forge response, code: " + forgeResponse.code());
}
}
public void peapplyOperations(TezosPreapplyBody tezosPreapplyBody) throws Exception {
Log.i(TAG, "new peapplyOperations request");
List<TezosPreapplyBody> tezosPreapplyBodyList = new ArrayList<>();
tezosPreapplyBodyList.add(tezosPreapplyBody);
Response<Void> preapplyResponse = tezosApi.preapplyOperations(tezosPreapplyBodyList).execute();
if (preapplyResponse.code() != 200) {
String error = "Preapply error: unknown error";
if (preapplyResponse.errorBody() != null) {
error = "Preapply error: " + preapplyResponse.errorBody().string();
}
Log.e(TAG, error);
throw new Exception(error);
}
}
public void injectOperations(String txForSend, SingleObserver<Object> injectObserver) {
requestsCount++;
Log.i(TAG, "new injectOperations request");
Single<Object> injectSingle = tezosApi.injectOperations(txForSend)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
injectSingle.subscribe(injectObserver);
}
}

View file

@ -16,4 +16,5 @@ class ServerURL {
static final String API_STELLAR_RESERVE = "https://horizon.sui.li/";
static final String API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/";
static final String API_BLOCKCHAIN_INFO = "https://blockchain.info/";
static final String API_DUCATUS = "https://ducapi.rocknblock.io/";
}

View file

@ -0,0 +1,35 @@
package com.tangem.data.network;
import com.tangem.data.network.model.TezosAccountResponse;
import com.tangem.data.network.model.TezosForgeBody;
import com.tangem.data.network.model.TezosHeaderResponse;
import com.tangem.data.network.model.TezosPreapplyBody;
import java.util.List;
import io.reactivex.Single;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
public interface TezosApi {
@GET(ServerApiTezos.TEZOS_ADDRESS)
Single<TezosAccountResponse> getAccount(@Path("address") String address);
@GET(ServerApiTezos.TEZOS_HEADER)
Call<TezosHeaderResponse> getHeader();
@GET(ServerApiTezos.TEZOS_MANAGER_KEY)
Single<String> getManagerKey(@Path("address") String address);
@POST(ServerApiTezos.TEZOS_FORGE_OPERATIONS)
Call<String> forgeOperations(@Body TezosForgeBody tezosForgeBody);
@POST(ServerApiTezos.TEZOS_PREAPPLY_OPERATIONS)
Call<Void> preapplyOperations(@Body List<TezosPreapplyBody> tezosPreapplyBodyList);
@POST(ServerApiTezos.TEZOS_INJECT_OPERATIONS)
Single<Object> injectOperations(@Body String txForSend);
}

View file

@ -0,0 +1,34 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class BitcoreBalance(
@SerializedName("confirmed")
var confirmed: Long? = null,
@SerializedName("unconfirmed")
var unconfirmed: Long? = null
)
data class BitcoreUtxo(
@SerializedName("mintTxid")
var mintTxid: String? = null,
@SerializedName("mintIndex")
var mintIndex: Int? = null,
@SerializedName("value")
var value: Long? = null,
@SerializedName("script")
var script: String? = null
)
data class BitcoreBalanceAndUnspents(
var balance: BitcoreBalance,
var unspents: List<BitcoreUtxo>
)
data class BitcoreSendResponse(
var txid: String? = null
)

View file

@ -0,0 +1,14 @@
package com.tangem.data.network.model;
import java.util.ArrayList;
import java.util.List;
public class BitcoreSendBody {
private List<String> rawTx;
public BitcoreSendBody(String tx) {
List<String> txList = new ArrayList<>();
txList.add(tx);
rawTx = txList;
}
}

View file

@ -35,7 +35,10 @@ data class BlockcypherTxref(
data class BlockcypherTx(
@SerializedName("hex")
var hex: String? = null
var hex: String? = null,
@SerializedName("addresses")
var addesses: List<String>? = null
)
data class BlockcypherFee(

View file

@ -12,27 +12,29 @@ data class InsightResponse(
@SerializedName("addrStr")
var addrStr: String = "",
// @SerializedName("2")
// var fee2: String = "",
//
// @SerializedName("3")
// var fee3: String = "",
//
// @SerializedName("6")
// var fee6: String = "",
@SerializedName("error")
var error: String = ""
)
data class InsightUtxo(
@SerializedName("txid")
var txid: String = "",
@SerializedName("satoshis")
var satoshis: Long? = null,
@SerializedName("height")
var height: Int? = null,
@SerializedName("vout")
var vout: Int? = null,
@SerializedName("2")
var fee2: String = "",
@SerializedName("3")
var fee3: String = "",
@SerializedName("6")
var fee6: String = "",
@SerializedName("rawtx")
var rawtx: String = "",
@SerializedName("error")
var error: String = ""
@SerializedName("scriptPubKey")
var scriptPubKey: String? = null
)

View file

@ -0,0 +1,25 @@
package com.tangem.data.network.model
data class TezosForgeBody(
val branch: String,
val contents: List<TezosOperationContent>
)
data class TezosOperationContent(
val kind: String,
val source: String,
val fee: String,
val counter: String,
val gas_limit: String,
val storage_limit: String,
val public_key: String? = null,
val destination: String? = null,
val amount: String? = null
)
data class TezosPreapplyBody(
val protocol: String,
val branch: String,
val contents: List<TezosOperationContent>,
val signature: String
)

View file

@ -0,0 +1,19 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class TezosAccountResponse(
@SerializedName("balance")
var balance: Long? = null,
@SerializedName("counter")
var counter: Long? = null
)
data class TezosHeaderResponse(
@SerializedName("protocol")
var protocol: String? = null,
@SerializedName("hash")
var hash: String? = null
)

View file

@ -38,6 +38,9 @@ interface NetworkComponent {
@get:Named(Server.ApiBlockchainInfo.URL_BLOCKCHAININFO)
val retrofitBlockchainInfo: Retrofit
@get:Named(Server.ApiDucatus.URL_DUCATUS)
val retrofitDucatus: Retrofit
@get:Named("socket")
val socket: Socket

View file

@ -133,6 +133,20 @@ internal class NetworkModule {
return builder.build()
}
@Singleton
@Provides
@Named(Server.ApiDucatus.URL_DUCATUS)
fun provideRetrofitDucatus(): Retrofit {
val builder = Retrofit.Builder()
.baseUrl(Server.ApiDucatus.URL_DUCATUS)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
if (BuildConfig.DEBUG)
builder.client(createOkHttpClient())
return builder.build()
}
private fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(createHttpLoggingInterceptor()).build()
}

View file

@ -7,10 +7,12 @@ import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.NfcV
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.util.Log
import android.view.*
import android.widget.PopupMenu
import android.widget.TextView
@ -20,14 +22,21 @@ import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProviders
import com.tangem.App
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.data.Logger
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.reader.TLV
import com.tangem.tangem_card.reader.TLVException
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.tasks.CustomReadCardTask
import com.tangem.tangem_card.tasks.ReadCardInfoTask
import com.tangem.tangem_sdk.android.data.PINStorage
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.android.reader.NfcVReader
import com.tangem.tangem_sdk.android.reader.ReadResult
import com.tangem.tangem_sdk.android.reader.ReadSlixTagTask
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.loadFromBundle
@ -45,10 +54,16 @@ import com.tangem.wallet.BuildConfig
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import com.tangem.wallet.xlmTag.XlmTagEngine
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.util.*
import kotlin.coroutines.CoroutineContext
class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback,
CardProtocol.Notifications, androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener,
@ -68,6 +83,11 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
private var zipFile: File? = null
private var unknownBlockchain = false
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.IO
private val scope = CoroutineScope(coroutineContext)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
return super.onCreateView(inflater, container, savedInstanceState)
@ -77,6 +97,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
super.onViewCreated(view, savedInstanceState)
rippleBackgroundNfc.startRippleAnimation()
// navigateToDestination(R.id.action_main_to_emptyIdFragment)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
@ -109,7 +130,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
tvBuyCards?.setText(spannable, TextView.BufferType.SPANNABLE)
llShoppingView?.setOnClickListener {
val uri = Uri.parse ("https://www.tangemcards.com")
val uri = Uri.parse("https://www.tangemcards.com")
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
@ -152,6 +173,8 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
return
}
NfcV.get(tag)?.let { onNfcVDiscovered(it, tag.id) }
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
@ -175,6 +198,44 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
}
}
private fun onNfcVDiscovered(nfcV: NfcV, uid: ByteArray) {
scope.launch {
when (val readResult = ReadSlixTagTask(NfcVReader(nfcV)).read()) {
is ReadResult.Failure -> (activity as MainActivity).nfcManager.notifyReadResult(false)
is ReadResult.Success -> {
try {
val tlvs = readResult.tlvs
val cardDataTlv = TLVList.fromBytes((tlvs.getTLV(TLV.Tag.TAG_CardData)).Value)
Log.v(TAG, "\n" + tlvs.getParsedTLVs(""))
val card = TangemCard(uid.toString())
card.batch = cardDataTlv.getTLV(TLV.Tag.TAG_Batch).asHexString
card.setIssuer(cardDataTlv.getTLV(TLV.Tag.TAG_Issuer_ID).Value.toString(), null)
card.blockchainID = Blockchain.StellarTag.id
card.walletPublicKey = tlvs.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value
card.status = TangemCard.Status.Loaded
card.tagSignature = tlvs.getTLV(TLV.Tag.TAG_Signature).Value
val ctx = TangemContext(card)
val engineCoin = XlmTagEngine(ctx)
engineCoin.defineWallet()
launch(Dispatchers.Main) {
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY,
R.id.action_main_to_tagFragment, bundle)
}
} catch (e: TLVException) {
e.printStackTrace()
Log.v(TAG, e.message)
}
}
}
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE }
}
@ -213,20 +274,31 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
card.status == TangemCard.Status.Loaded -> lastTag?.let {
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin != null) {
engineCoin.defineWallet()
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY,
R.id.action_main_to_loadedWalletFragment, bundle)
if (card.isIDCard) {
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateToDestination(R.id.action_main_to_idFragment, bundle)
} else {
engineCoin.defineWallet()
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY,
R.id.action_main_to_loadedWalletFragment, bundle)
}
} else {
showUnkownBlockchainWarning()
}
}
card.status == TangemCard.Status.Empty -> {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateToDestination(R.id.action_main_to_emptyWalletFragment, bundle)
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin != null) {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateToDestination(R.id.action_main_to_emptyWalletFragment, bundle)
} else {
showUnkownBlockchainWarning()
}
}
card.status == TangemCard.Status.Purged -> Toast.makeText(context, R.string.main_screen_erased_wallet, Toast.LENGTH_SHORT).show()
card.status == TangemCard.Status.NotPersonalized -> Toast.makeText(context, R.string.main_screen_not_personalized, Toast.LENGTH_SHORT).show()

View file

@ -0,0 +1,212 @@
package com.tangem.ui.fragment.additional
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.tangem.data.network.ServerApiCommon
import com.tangem.server_android.ServerApiTangem
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.wallet.LoadedWalletFragment
import com.tangem.ui.fragment.wallet.LoadedWalletViewModel
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.*
import com.tangem.wallet.xlmTag.XlmTagEngine
import kotlinx.android.synthetic.main.fr_loaded_wallet.*
import kotlinx.android.synthetic.main.layout_btn_details.*
import kotlinx.android.synthetic.main.layout_tangem_card.*
class TagFragment : BaseFragment(), NavigationResultListener {
override val layoutId = R.layout.fragment_tag
private lateinit var viewModel: LoadedWalletViewModel
private lateinit var ctx: TangemContext
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private var serverApiTangem: ServerApiTangem = ServerApiTangem()
private var requestCounter: Int = 0
set(value) {
field = value
LOG.i(LoadedWalletFragment.TAG, "requestCounter, set $field")
if (field <= 0) {
LOG.e(LoadedWalletFragment.TAG, "+++++++++++ FINISH REFRESH")
if (srl != null && srl.isRefreshing)
srl.isRefreshing = false
} else if (srl != null && !srl.isRefreshing)
srl.isRefreshing = true
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val engine = XlmTagEngine(ctx)
btnLoad.visibility = View.GONE
btnDetails.visibility = View.GONE
btnExtract.text = getString(R.string.tag_claim)
btnExtract.isEnabled = false //TODO: enable when we implement extraction
btnExtract.backgroundTintList =
ContextCompat.getColorStateList(requireContext(), R.color.btn_dark)
ivTangemCard.setImageResource(R.drawable.card_tgslix)
tvBalance.setSingleLine(!engine.needMultipleLinesForBalance())
tvWallet.text = ctx.coinData.wallet
tvWallet.setOnClickListener { shareWallet() }
btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine.walletExplorerUri)) }
btnCopy.setOnClickListener { shareWallet() }
btnNewScan.setOnClickListener { navigateUp() }
srl?.setOnRefreshListener { refresh(true) }
requestBalanceAndUnspentTransactions()
}
private fun shareWallet() {
val txtShare = ctx.coinData.wallet
val clipboard = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare)
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
}
private fun update() {
ctx.coinData.setIsBalanceEqual(true)
if (srl.isRefreshing) {
tvBalanceLine1.setTextColor(resources.getColor(R.color.primary))
tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain)
tvBalanceLine2.text = ""
tvBalance.text = ""
tvBalanceEquivalent.text = ""
} else {
val validator = BalanceValidator()
validator.check(ctx, false)
context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) }
tvBalanceLine1?.text = getString(validator.firstLine)
tvBalanceLine2?.text = getString(validator.getSecondLine(false))
}
val engine = CoinEngineFactory.create(ctx)
when {
engine!!.hasBalanceInfo() -> {
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine.balanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine.balanceHTML)
tvBalance.text = html
tvBalanceEquivalent.text = engine.balanceEquivalent
}
ctx.card?.offlineBalance != null -> {
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine.offlineBalanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine.offlineBalanceHTML)
tvBalance.text = html
}
else -> tvBalance.text = ""
}
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(ctx.blockchainName)
tvBlockchain.text = html
}
private fun requestBalanceAndUnspentTransactions() {
if (UtilHelper.isOnline(context as Activity)) {
val coinEngine = CoinEngineFactory.create(ctx)
requestCounter++
coinEngine!!.requestBalanceAndUnspentTransactions(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
LOG.i(TAG, "requestBalanceAndUnspentTransactions onComplete: $success, request counter $requestCounter")
if (activity == null) return
requestCounter--
if (!success) {
LOG.e(TAG, "requestBalanceAndUnspentTransactions ctx.error: " + ctx.error)
}
update()
}
override fun onProgress() {
if (activity == null) return
LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress")
// update()
}
override fun allowAdvance(): Boolean {
return try {
context?.let { UtilHelper.isOnline(it) }!!
} catch (e: KotlinNullPointerException) {
e.printStackTrace()
false
}
}
}
)
} else {
ctx.error = getString(R.string.general_error_no_connection)
update()
}
}
private fun refresh(clearData: Boolean = true) {
if (ctx.card == null) return
// clear all card data and request again
ctx.coinData.clearInfo()
if (clearData) {
ctx.error = null
ctx.message = null
}
LOG.w(TAG, "============= START REFRESH")
requestCounter = 0
srl?.isRefreshing = true
update()
ctx.coinData.setIsBalanceEqual(true)
requestBalanceAndUnspentTransactions()
if (requestCounter == 0) {
// if no connection and no requests posted
srl?.isRefreshing = false
update()
}
}
companion object {
val TAG: String = TagFragment::class.java.simpleName
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.PictureResult
import com.otaliastudios.cameraview.controls.Facing
import com.tangem.ui.fragment.BaseFragment
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_camera.*
class CameraFragment : BaseFragment() {
override val layoutId = R.layout.fragment_camera
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
cvCamera?.setLifecycleOwner(viewLifecycleOwner)
cvCamera?.addCameraListener(object : CameraListener() {
override fun onPictureTaken(result: PictureResult) { // Picture was taken!
result.toBitmap(200, 200) {
navigateBackWithResult(Activity.RESULT_OK, bundleOf(PHOTO_KEY to it))
}
}
})
ivTakePicture?.setOnClickListener { cvCamera?.takePictureSnapshot() }
ivFlipCamera?.setOnClickListener { flipCamera() }
}
private fun flipCamera() {
if (cvCamera?.facing == Facing.BACK) {
cvCamera?.facing = Facing.FRONT
} else {
cvCamera?.facing = Facing.BACK
}
}
companion object{
const val PHOTO_KEY = "photo"
}
}

View file

@ -0,0 +1,462 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.BitmapFactory
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.tangem.App
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.data.network.ServerApiCommon
import com.tangem.server_android.ServerApiTangem
import com.tangem.server_android.model.CardVerifyAndGetInfo
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.VerifyCardTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.wallet.LoadedWalletViewModel
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.*
import kotlinx.android.synthetic.main.fragment_id.*
import kotlinx.android.synthetic.main.layout_btn_details.*
import kotlinx.android.synthetic.main.layout_id.*
import kotlinx.android.synthetic.main.layout_tangem_card.*
import kotlinx.android.synthetic.main.layout_tangem_card.rlProgressBar
import net.i2p.crypto.eddsa.Utils
import java.util.*
import kotlin.concurrent.timerTask
class IdFragment : BaseFragment(), NfcAdapter.ReaderCallback,
CardProtocol.Notifications, SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
val TAG: String = IdFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_id
private lateinit var ctx: TangemContext
private lateinit var mpSecondScanSound: MediaPlayer
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private var serverApiTangem: ServerApiTangem = ServerApiTangem()
private var lastTag: Tag? = null
private var lastReadSuccess = true
private var verifyCardTask: VerifyCardTask? = null
private var requestPIN2Count = 0
private var timerHideErrorAndMessage: Timer? = null
private var newPIN = ""
private var newPIN2 = ""
private var photo: ByteArray? = null
private var cardProtocol: CardProtocol? = null
private var refreshAction: Runnable? = null
private var hasIdInfo: Boolean? = null
private var toast: Toast? = null
private var requestCounter: Int = 0
set(value) {
field = value
LOG.i(TAG, "requestCounter, set $field")
if (field <= 0) {
LOG.e(TAG, "+++++++++++ FINISH REFRESH")
if (srl != null && srl.isRefreshing)
srl.isRefreshing = false
} else if (srl != null && !srl.isRefreshing)
srl.isRefreshing = true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
lastTag = activity?.intent?.getParcelableExtra(Constant.EXTRA_LAST_DISCOVERED_TAG)
if (arguments?.containsKey(NfcAdapter.EXTRA_TAG) == true) {
val tag = arguments!!.getParcelable<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) onTagDiscovered(tag)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpSecondScanSound = MediaPlayer.create(activity, R.raw.scan_card_sound)
tvIdNumber?.text = "ID # ${Utils.bytesToHex(ctx.card.cid)}"
if (hasIdInfo == false) rlToolButtons?.visibility = View.VISIBLE
// set listeners
srl.setOnRefreshListener { refresh() }
btnDetails.setOnClickListener {
if (cardProtocol != null) {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateForResult(Constant.REQUEST_CODE_VERIFY_CARD,
R.id.action_idFragment_to_verifyCard, bundle)
} else {
(activity as MainActivity).toastHelper
.showSingleToast(context, getString(R.string.general_notification_scan_again_to_verify))
}
}
btnIssueNewId?.setOnClickListener {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateToDestination(R.id.action_idFragment_to_issueNewIdFragment, bundle)
}
if (hasIdInfo == null) tvBalanceLine1?.text = "Reading... Hold the card firmly"
btnNewScan.setOnClickListener {
navigateUp()
}
// request card verify and get info listener
val cardVerifyAndGetInfoListener: ServerApiTangem.CardVerifyAndGetInfoListener = object : ServerApiTangem.CardVerifyAndGetInfoListener {
override fun onSuccess(cardVerifyAndGetArtworkResponse: CardVerifyAndGetInfo.Response?) {
LOG.i(TAG, "cardVerifyAndGetInfoListener onSuccess")
if (activity == null || !UtilHelper.isOnline(activity!!)) return
val result = cardVerifyAndGetArtworkResponse?.results!![0]
if (result.error != null) {
ctx.card!!.isOnlineVerified = false
return
}
ctx.card!!.isOnlineVerified = result.passed
requestCounter--
updateViews()
if (!result.passed) return
if (App.localStorage.checkBatchInfoChanged(ctx.card!!, result)) {
LOG.w(TAG, "Batch ${result.batch} info changed to '$result'")
ivTangemCard.setImageBitmap(App.localStorage.getCardArtworkBitmap(ctx.card!!))
App.localStorage.applySubstitution(ctx.card!!)
refresh()
}
}
override fun onFail(message: String?) {
LOG.i(TAG, "cardVerifyAndGetInfoListener onFail")
if (activity == null || !UtilHelper.isOnline(activity!!)) return
requestCounter--
updateViews()
}
}
serverApiTangem.setCardVerifyAndGetInfoListener(cardVerifyAndGetInfoListener)
refresh()
startVerify(lastTag)
}
override fun onPause() {
super.onPause()
if (timerHideErrorAndMessage != null) {
timerHideErrorAndMessage!!.cancel()
timerHideErrorAndMessage = null
}
}
override fun onStop() {
srl?.removeCallbacks(refreshAction)
toast?.cancel()
super.onStop()
}
override fun onTagDiscovered(tag: Tag) {
startVerify(tag)
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE }
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
verifyCardTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar?.post {
rlProgressBar?.visibility = View.GONE
this.cardProtocol = cardProtocol
if (!cardProtocol.card.isWalletPublicKeyValid)
refresh()
else
updateViews()
if (cardProtocol.card.isIDCard) {
if (cardProtocol.card.hasIDCardData()) {
ctx.card = cardProtocol.card
tvBalanceLine1?.visibility = View.VISIBLE
val idCardData = ctx.card.idCardData
tvFullName.text = idCardData.fullName
tvBirthDate.text = idCardData.birthday
tvGender.text = "Sex: ${idCardData.gender}"
ivPhoto.setImageBitmap(BitmapFactory.decodeByteArray(idCardData.photo, 0, idCardData.photo.size))
val inputs = "${idCardData.fullName};${idCardData.birthday}${idCardData.gender}"
val info = inputs.toByteArray() + idCardData.photo
ctx.card.idHash = Util.calculateSHA256(info)
refresh()
} else {
rlToolButtons?.visibility = View.VISIBLE
tvBalanceLine1?.text = resources.getString(R.string.id_empty).toUpperCase(Locale.ENGLISH)
tvBalanceLine1?.setTextSize(18F)
tvBalanceLine1?.visibility = View.VISIBLE
hasIdInfo = false
}
}
mpSecondScanSound.start()
}
} else {
// remove last UIDs because of error and no card read
rlProgressBar?.post {
lastReadSuccess = false
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported)
if (!NoExtendedLengthSupportDialog.allReadyShowed)
activity?.supportFragmentManager?.let { NoExtendedLengthSupportDialog().show(it, NoExtendedLengthSupportDialog.TAG) }
else {
toast = Toast.makeText(activity, R.string.general_notification_scan_again, Toast.LENGTH_SHORT)
toast?.show()
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
verifyCardTask = null
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadWait(msec: Int) {
WaitSecurityDelayDialog.onReadWait(activity as AppCompatActivity?, msec)
}
override fun onReadBeforeRequest(timeout: Int) {
WaitSecurityDelayDialog.onReadBeforeRequest(activity as AppCompatActivity?, timeout)
}
override fun onReadAfterRequest() {
WaitSecurityDelayDialog.onReadAfterRequest(activity)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
}
fun updateViews() {
if (activity == null || this.view == null) return
if (timerHideErrorAndMessage != null) {
timerHideErrorAndMessage!!.cancel()
timerHideErrorAndMessage = null
}
if (ctx.card?.idCardData?.photo != null && (photo == null)) {
photo = ctx.card.idCardData.photo
ivPhoto.setImageBitmap(BitmapFactory.decodeByteArray(photo, 0, photo!!.size))
}
if (ctx.hasError()) {
tvError?.visibility = View.VISIBLE
tvError?.text = ctx.error
} else {
tvError?.visibility = View.GONE
tvError?.text = ""
}
if (ctx.message == null || ctx.message.isEmpty()) {
tvMessage?.text = ""
tvMessage?.visibility = View.GONE
} else {
tvMessage?.text = ctx.message
tvMessage?.visibility = View.VISIBLE
}
if (tvError?.visibility == View.VISIBLE || tvMessage?.visibility == View.VISIBLE) {
timerHideErrorAndMessage = Timer()
timerHideErrorAndMessage?.schedule(
timerTask {
activity?.runOnUiThread {
tvMessage?.visibility = View.GONE
tvError?.visibility = View.GONE
// clear only already viewed messages
if (tvMessage?.text == ctx.message) ctx.message = null
if (tvError?.text == ctx.error) ctx.error = null
}
},
5000)
}
if (srl.isRefreshing && ctx.card?.hasIDCardData() != false) {
tvBalanceLine1.setTextColor(resources.getColor(R.color.primary))
tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain)
tvBalanceLine2.text = ""
} else if (ctx.card?.hasIDCardData() != false) {
val validator = BalanceValidator()
// TODO why attest=false?
validator.check(ctx, false)
context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) }
tvBalanceLine1?.text = getString(validator.firstLine)
}
if (ctx.card.hasIDCardData()) {
val idCardData = ctx.card.idCardData
tvFullName.text = idCardData.fullName
tvBirthDate.text = idCardData.birthday
tvGender.text = "Sex: ${idCardData.gender}"
ivPhoto.setImageBitmap(BitmapFactory.decodeByteArray(idCardData.photo, 0, idCardData.photo.size))
rlToolButtons?.visibility = View.GONE
}
}
private fun refresh() {
if (ctx.card == null) return
// clear all card data and request again
ctx.coinData.clearInfo()
ctx.error = null
ctx.message = null
LOG.w(TAG, "============= START REFRESH")
requestCounter = 0
srl?.isRefreshing = true
updateViews()
// Bitcoin, Litecoin, BitcoinCash, Stellar
if (ctx.blockchain == Blockchain.Bitcoin || ctx.blockchain == Blockchain.BitcoinTestNet ||
ctx.blockchain == Blockchain.Litecoin || ctx.blockchain == Blockchain.BitcoinCash ||
ctx.blockchain == Blockchain.Stellar || ctx.blockchain == Blockchain.StellarTestNet || ctx.blockchain == Blockchain.StellarAsset) {
ctx.coinData.setIsBalanceEqual(true)
}
requestVerifyAndGetInfo()
if (ctx.card.hasIDCardData()) requestBalanceAndUnspentTransactions()
if (requestCounter == 0) {
// if no connection and no requests posted
srl?.isRefreshing = false
updateViews()
}
}
private fun requestBalanceAndUnspentTransactions() {
if (UtilHelper.isOnline(context as Activity)) {
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine?.defineWallet()
requestCounter++
coinEngine!!.requestBalanceAndUnspentTransactions(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
LOG.i(TAG, "requestBalanceAndUnspentTransactions onComplete: $success, request counter $requestCounter")
if (activity == null) return
requestCounter--
if (!success) {
LOG.e(TAG, "requestBalanceAndUnspentTransactions ctx.error: " + ctx.error)
}
updateViews()
}
override fun onProgress() {
if (activity == null) return
LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress")
updateViews()
}
override fun allowAdvance(): Boolean {
return try {
context?.let { UtilHelper.isOnline(it) }!!
} catch (e: KotlinNullPointerException) {
e.printStackTrace()
false
}
}
}
)
} else {
ctx.error = getString(R.string.general_error_no_connection)
updateViews()
}
}
private fun requestVerifyAndGetInfo() {
if (UtilHelper.isOnline(context as Activity)) {
if ((ctx.card!!.isOnlineVerified == null || !ctx.card!!.isOnlineVerified)) {
LOG.i(TAG, "requestVerifyAndGetInfo")
requestCounter++
serverApiTangem.cardVerifyAndGetInfo(ctx.card)
}
} else {
ctx.error = getString(R.string.general_error_no_connection)
updateViews()
}
}
private fun startVerify(tag: Tag?) {
try {
val isoDep = IsoDep.get(tag)
val uid = tag?.id
val sUID = Util.byteArrayToHexString(uid)
if (ctx.card.uid != sUID || cardProtocol != null) {
(activity as MainActivity).nfcManager.ignoreTag(isoDep.tag)
return
}
if (lastReadSuccess)
isoDep.timeout = 1000
else
isoDep.timeout = 65000
verifyCardTask = VerifyCardTask(ctx.card, NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, App.firmwaresStorage, this)
verifyCardTask?.start()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View file

@ -0,0 +1,253 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import com.tangem.tangem_card.util.Util
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import com.tangem.wallet.ethID.EthIdEngine
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_issue_new_id.*
import java.io.ByteArrayOutputStream
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*
class IssueNewIdFragment : BaseFragment(), NavigationResultListener {
lateinit var ctx: TangemContext
private var photo: Bitmap? = null
private var photoInBytes: ByteArray? = null
private var gender: Gender? = null
private var userIdData: UserIdData? = null
override val layoutId = R.layout.fragment_issue_new_id
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
ivPhoto?.setOnClickListener {
navigateForResult(GET_PHOTO_REQUEST_CODE, R.id.action_issueNewIdFragment_to_cameraFragment)
}
if (photo != null) ivPhoto?.setImageBitmap(photo)
btnConfirm?.setOnClickListener {
if (!checkIfInfoProvided()) {
Toast.makeText(context, "Fill in all data first", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
try {
processData()
} catch (exception: Exception) {
Toast.makeText(context, "Check for correctness of data", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val bundle = bundleOf(USER_ID_DATA to userIdData)
ctx.saveToBundle(bundle)
navigateToDestination(R.id.action_issueNewIdFragment_to_validateIdFragment, bundle)
}
rbFemale?.setOnClickListener { onRadioButtonClicked(it) }
rbMale?.setOnClickListener { onRadioButtonClicked(it) }
DateInputMask(etBirthDate).listen()
// etBirthDate?.addTextChangedListener(MaskWatcher("##/##/####"))
}
private fun onRadioButtonClicked(view: View) {
if (view is RadioButton) {
when (view.getId()) {
R.id.rbFemale ->
if (view.isChecked) {
gender = Gender.F
}
R.id.rbMale ->
if (view.isChecked) {
gender = Gender.M
}
}
}
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == GET_PHOTO_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
photo = data?.getParcelable<Bitmap>(CameraFragment.PHOTO_KEY)
ivPhoto?.setImageBitmap(photo)
val out = ByteArrayOutputStream()
photo!!.compress(Bitmap.CompressFormat.JPEG, 100, out)
photoInBytes = out.toByteArray()
}
}
}
private fun checkIfInfoProvided(): Boolean {
return (etName.text.isNotBlank() && etSurname.text.isNotBlank() && etBirthDate.text.isNotBlank() &&
(photo != null) && (gender != null))
}
private fun processData() {
val name = etName.text.toString()
val lastName = etSurname.text.toString()
val birthDate = convertDate(etBirthDate.text)
userIdData = UserIdData(name, lastName, birthDate, gender.toString(), photo!!)
val issuerExpireDate = processIssueExpireDate()
ctx.card.setTlvIDCardData(
"$name $lastName",
birthDate,
gender.toString(),
photoInBytes,
issuerExpireDate.issueDate,
issuerExpireDate.expireDate,
(CoinEngineFactory.create(ctx) as? EthIdEngine)?.approvalAddress
)
val inputs = "$name $lastName;$birthDate${gender.toString()}"
val info = inputs.toByteArray() + photoInBytes!!
ctx.card.idHash = Util.calculateSHA256(info)
}
private fun convertDate(inputDate: CharSequence): String {
val calendar = Calendar.getInstance()
calendar.clear()
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH)
try {
calendar.time = sdf.parse(inputDate.toString())
} catch (exception: Error) {
etBirthDate.setTextColor(ContextCompat.getColor(requireContext(), R.color.bg_err))
}
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.timeZone = SimpleTimeZone(0, "UTC")
val dateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH)
dateFormat.timeZone = SimpleTimeZone(0, "UTC")
return dateFormat.format(calendar.time)
}
private fun processIssueExpireDate(): IssuerExpireDate {
val dt = Date()
val issueDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt)
dt.year = dt.year + 10
val expireDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt)
return IssuerExpireDate(issueDate, expireDate)
}
data class IssuerExpireDate(val issueDate: String, val expireDate: String)
companion object {
const val SEPARATOR = ";"
const val COMPUTED_PUB_KEY = "ComputedPubKey"
const val GET_PHOTO_REQUEST_CODE = "GetPhoto"
const val USER_ID_DATA = "UserIdData"
}
}
enum class Gender { M, F }
@Parcelize
data class UserIdData(
val firstName: String,
val lastName: String,
val birthDate: String,
val gender: String,
val photo: Bitmap
) : Parcelable
class DateInputMask(val input: EditText) {
fun listen() {
input.addTextChangedListener(dateEntryWatcher)
}
private val dateEntryWatcher = object : TextWatcher {
var edited = false
val dividerCharacter = "/"
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (edited) {
edited = false
return
}
var working = getEditText()
working = manageDateDivider(working, 2, start, before)
working = manageDateDivider(working, 5, start, before)
edited = true
input.setText(working)
input.setSelection(input.text.length)
}
private fun manageDateDivider(working: String, position: Int, start: Int, before: Int): String {
if (working.length == position) {
return if (before <= position && start < position)
working + dividerCharacter
else
working.dropLast(1)
}
return working
}
private fun getEditText(): String {
return if (input.text.length >= 10)
input.text.toString().substring(0, 10)
else
input.text.toString()
}
override fun afterTextChanged(s: Editable) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
}
}
class MaskWatcher(private val mask: String) : TextWatcher {
private var isRunning = false
private var isDeleting = false
override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) {
isDeleting = count > after
}
override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(editable: Editable) {
if (isRunning || isDeleting) {
return
}
isRunning = true
val editableLength = editable.length
if (editableLength < mask.length) {
if (mask[editableLength] != '#') {
editable.append(mask[editableLength])
} else if (mask[editableLength - 1] != '#') {
editable.insert(editableLength - 1, mask, editableLength - 1, editableLength)
}
}
if (editableLength > mask.length) editable.delete(editable.lastIndex, editableLength)
isRunning = false
}
companion object {
fun buildCpf(): MaskWatcher {
return MaskWatcher("###.###.###-##")
}
}
}

View file

@ -0,0 +1,323 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.tangem.App
import com.tangem.Constant
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.OneTouchSignTask
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.data.PINStorage
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.asBundle
import com.tangem.ui.SignTransactionFragment
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
class ValidateIdFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = ValidateIdFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_validate_id
private lateinit var ctx: TangemContext
private lateinit var mpFinishSignSound: MediaPlayer
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var signTransactionTask: OneTouchSignTask? = null
private lateinit var amount: CoinEngine.Amount
private lateinit var fee: CoinEngine.Amount
private var isIncludeFee = true
private var outAddressStr: String? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
}
override fun onPause() {
signTransactionTask?.cancel(true)
super.onPause()
}
override fun onStop() {
signTransactionTask?.cancel(true)
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
val uid = tag.id
val sUID = Util.byteArrayToHexString(uid)
if (lastReadSuccess)
isoDep.timeout = ctx.card.pauseBeforePIN2 + 5000
else
isoDep.timeout = ctx.card.pauseBeforePIN2 + 65000
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine?.defineWallet()
coinEngine?.setOnNeedSendTransaction { tx ->
if (tx != null) {
val data = Bundle()
ctx.saveToBundle(data)
data.putByteArray(Constant.EXTRA_TX, tx)
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION_,
R.id.action_validateIdFragment_to_writeIdFragment,
data)
}
}
coinEngine?.requestBalanceAndUnspentTransactions(object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean?) {
val transactionToSign = coinEngine.constructTransaction(null, null, true, null)
val transaction = object : OneTouchSignTask.TransactionToSign{
override fun isIssuerCanSignTransaction(card: TangemCard?): Boolean {
return false
}
override fun isIssuerCanSignData(card: TangemCard?): Boolean {
return false
}
override fun getHashAlgToSign(card: TangemCard?): String {
return transactionToSign.hashAlgToSign
}
override fun getRawDataToSign(card: TangemCard?): ByteArray {
return transactionToSign.rawDataToSign
}
override fun isSigningOnCardSupported(card: TangemCard?): Boolean {
return true
}
override fun onSignCompleted(card: TangemCard?, signature: ByteArray?) {
transactionToSign.onSignCompleted(signature)
}
override fun getHashesToSign(card: TangemCard?): Array<ByteArray> {
return transactionToSign.hashesToSign
}
override fun getIssuerTransactionSignature(card: TangemCard?, dataToSignByIssuer: ByteArray?): ByteArray {
return transactionToSign.getIssuerTransactionSignature(dataToSignByIssuer)
}
}
PINStorage.setPIN2(PINStorage.getDefaultPIN2())
signTransactionTask = OneTouchSignTask(NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, this@ValidateIdFragment, transaction)
signTransactionTask?.start()
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return true
}
})
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar?.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
signTransactionTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar?.post { rlProgressBar?.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
} else {
lastReadSuccess = false
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show()
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
signTransactionTask = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
}
}

View file

@ -0,0 +1,292 @@
package com.tangem.ui.fragment.id
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.tangem.App
import com.tangem.Constant
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.WriteIDCardTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.asBundle
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.tangemAccess.fragment_sign_transaction.*
class WriteIdFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = WriteIdFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_write_id
private lateinit var ctx: TangemContext
private lateinit var tx: ByteArray
private lateinit var mpFinishSignSound: MediaPlayer
private var toast: Toast? = null
private var idWasWritten = false
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var writeIDCardTask: WriteIDCardTask? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
tx = arguments?.getByteArray(Constant.EXTRA_TX)!!
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
tvCardID.text = ctx.card!!.cidDescription
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
tvProgressBar?.text = "Writing..."
}
override fun onPause() {
writeIDCardTask?.cancel(true)
super.onPause()
}
override fun onStop() {
writeIDCardTask?.cancel(true)
toast?.cancel()
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
val uid = tag.id
val sUID = Util.byteArrayToHexString(uid)
if (sUID == ctx.card.uid || !idWasWritten) {
if (lastReadSuccess)
isoDep.timeout = ctx.card.pauseBeforePIN2 + 5000
else
isoDep.timeout = ctx.card.pauseBeforePIN2 + 65000
writeIDCardTask = WriteIDCardTask(ctx.card, NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, ctx.card.idCardData, ctx.card.issuer.privateDataKey, this)
writeIDCardTask?.start()
} else
(activity as MainActivity).nfcManager.ignoreTag(isoDep.tag)
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar?.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
writeIDCardTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar?.post { rlProgressBar?.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
idWasWritten = true
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine!!.requestSendTransaction(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
if (success) {
navigateUp(R.id.main)
} else {
toast?.cancel()
navigateUp(R.id.issueNewIdFragment)
}
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return UtilHelper.isOnline(requireContext())
}
},
tx
)
} else {
lastReadSuccess = false
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
if (!idWasWritten) {
toast = Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_SHORT)
toast?.show()
}
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
writeIDCardTask = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,23 +1,26 @@
package com.tangem.wallet
import android.util.Log
import com.tangem.wallet.btc.BtcEngine
import com.tangem.wallet.eth.EthEngine
import com.tangem.wallet.token.TokenEngine
import com.tangem.wallet.bch.BtcCashEngine
import com.tangem.data.Blockchain
import com.tangem.wallet.eos.EosEngine
import com.tangem.wallet.bch.BtcCashEngine
import com.tangem.wallet.binance.BinanceEngine
import com.tangem.wallet.btc.BtcEngine
import com.tangem.wallet.cardano.CardanoData
import com.tangem.wallet.cardano.CardanoEngine
import com.tangem.wallet.ducatus.DucatusEngine
import com.tangem.wallet.eos.EosEngine
import com.tangem.wallet.eth.EthEngine
import com.tangem.wallet.ethID.EthIdEngine
import com.tangem.wallet.ltc.LtcEngine
import com.tangem.wallet.matic.MaticTokenEngine
import com.tangem.wallet.nftToken.NftTokenEngine
import com.tangem.wallet.rsk.RskEngine
import com.tangem.wallet.rsk.RskTokenEngine
import com.tangem.wallet.token.TokenEngine
import com.tangem.wallet.tezos.TezosEngine
import com.tangem.wallet.xlm.XlmAssetEngine
import com.tangem.wallet.xlm.XlmEngine
import com.tangem.wallet.xlmTag.XlmTagEngine
import com.tangem.wallet.xrp.XrpEngine
/**
@ -37,6 +40,7 @@ object CoinEngineFactory {
Blockchain.Bitcoin, Blockchain.BitcoinTestNet -> BtcEngine()
Blockchain.BitcoinCash -> BtcCashEngine()
Blockchain.Ethereum, Blockchain.EthereumTestNet -> EthEngine()
Blockchain.EthereumId -> EthIdEngine()
Blockchain.Token -> TokenEngine()
Blockchain.NftToken -> NftTokenEngine()
Blockchain.Litecoin -> LtcEngine()
@ -48,7 +52,10 @@ object CoinEngineFactory {
Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine()
Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine()
Blockchain.StellarAsset -> XlmAssetEngine()
Blockchain.StellarTag -> XlmTagEngine()
Blockchain.Eos -> EosEngine()
Blockchain.Ducatus -> DucatusEngine()
Blockchain.Tezos -> TezosEngine()
else -> null
}
}
@ -62,6 +69,8 @@ object CoinEngineFactory {
BtcEngine(context)
else if (Blockchain.Ethereum == context.blockchain || Blockchain.EthereumTestNet == context.blockchain)
EthEngine(context)
else if (Blockchain.EthereumId == context.blockchain)
EthIdEngine(context)
else if (Blockchain.Token == context.blockchain)
TokenEngine(context)
else if (Blockchain.NftToken == context.blockchain)
@ -84,8 +93,14 @@ object CoinEngineFactory {
XlmEngine(context)
else if (Blockchain.StellarAsset == context.blockchain)
XlmAssetEngine(context)
else if (Blockchain.StellarTag == context.blockchain)
XlmTagEngine(context)
else if (Blockchain.Eos == context.blockchain)
EosEngine(context)
else if (Blockchain.Ducatus == context.blockchain)
DucatusEngine(context)
else if (Blockchain.Tezos == context.blockchain)
TezosEngine(context)
else
return null
} catch (e: Exception) {

View file

@ -36,6 +36,9 @@ public class TangemContext {
return Blockchain.Token;
}
}
if (blockchain == Blockchain.Ethereum && card.isIDCard()) {
return Blockchain.EthereumId;
}
if ((blockchain == Blockchain.Rootstock) && card.isToken()) {
return Blockchain.RootstockToken;
}
@ -55,14 +58,16 @@ public class TangemContext {
public String getBlockchainName() {
Blockchain blockchain = getBlockchain();
if (blockchain == Blockchain.Token || blockchain == Blockchain.RootstockToken) {
String token = card.getTokenSymbol();
return token + " <br><small><small> " + getBlockchain().getOfficialName() + " smart contract token</small></small>";
return card.getTokenSymbol()+ "<br><small><small> " + getBlockchain().getOfficialName() + " smart contract token</small></small>";
}
if (blockchain == Blockchain.NftToken) {
return card.getTokenSymbol().substring(4) + " <br><small><small> " + getBlockchain().getOfficialName() + " NFT token</small></small>";
return card.getTokenSymbol().substring(4) + "<br><small><small> " + getBlockchain().getOfficialName() + " non-fungible token</small></small>";
}
if (blockchain == Blockchain.StellarAsset) {
return card.getTokenSymbol() + " <br><small><small> " + getBlockchain().getOfficialName() + " asset</small></small>";
return card.getTokenSymbol() + "<br><small><small> " + getBlockchain().getOfficialName() + " asset</small></small>";
}
if (blockchain == Blockchain.StellarTag) {
return "TANGEM TAG<br><small><small> "+ getBlockchain().getOfficialName() + " non-fungible token </small></small>";
}
return blockchain.getOfficialName();
}

View file

@ -566,7 +566,7 @@ public final class Transaction {
public static Script buildOutput(String address) throws BitcoinException {
//noinspection TryWithIdenticalCatches
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48) { //0 for BTC/BCH 1 address | 48 for LTC L address
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48 || addressWithCheckSumAndNetworkCode[0] == 49) { //0 for BTC/BCH 1 address | 48 for LTC L address | 49 for Ducatus
return buildOutputP2H(address);
}
@ -601,7 +601,7 @@ public final class Transaction {
//noinspection TryWithIdenticalCatches
try {
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48) {
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48 && addressWithCheckSumAndNetworkCode[0] != 49) {
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address);
}

View file

@ -24,6 +24,7 @@ import com.tangem.wallet.TangemContext;
import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONArray;
import org.json.JSONException;
@ -274,11 +275,11 @@ public class BtcCashEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -424,7 +425,15 @@ public class BtcCashEngine extends CoinEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -53,7 +53,7 @@ public class BinanceData extends CoinData {
}
public CoinEngine.Amount getBalance() {
return new CoinEngine.Amount(balance, "BNB");
return balance == null ? null : new CoinEngine.Amount(balance, "BNB");
}
public void setBalance(String balance) {

View file

@ -247,11 +247,11 @@ public class BinanceEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -22,18 +22,19 @@ public class BtcData extends CoinData {
//for blockchain.info
private boolean hasUnconfirmed = false;
public String getUnspentInputsDescription() {
public Unspents getUnspentInputsDescription() {
try {
int gatheredUnspents = 0;
if (unspentTransactions == null) return "";
if (unspentTransactions == null) return null;
for (int i = 0; i < unspentTransactions.size(); i++) {
if (unspentTransactions.get(i).script != null && unspentTransactions.get(i).script.length() > 1)
gatheredUnspents++;
}
return unspentTransactions.size() + " unspents (" + gatheredUnspents + " received)";
return new Unspents(unspentTransactions.size(), gatheredUnspents);
} catch (Exception e) {
e.printStackTrace();
return "";
return null;
}
}

View file

@ -310,11 +310,11 @@ public class BtcEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -437,7 +437,15 @@ public class BtcEngine extends CoinEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -0,0 +1,3 @@
package com.tangem.wallet.btc
data class Unspents(val unspetns: Int, val gatheredUnspents: Int)

View file

@ -105,7 +105,7 @@ public class CardanoData extends CoinData {
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
return new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace");
return balance == null ? null : new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace");
}
public void setBalance(Long balance) {

View file

@ -251,11 +251,11 @@ public class CardanoEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {
@ -503,7 +503,7 @@ public class CardanoEngine extends CoinEngine {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw;
return signingMethod == TangemCard.SigningMethod.Sign_Hash;
}
@Override

View file

@ -5,8 +5,13 @@ import android.text.InputFilter;
import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiBitcore;
import com.tangem.data.network.ServerApiInsight;
import com.tangem.data.network.model.BitcoreBalanceAndUnspents;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
@ -24,11 +29,11 @@ import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
@ -36,6 +41,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class DucatusEngine extends BtcEngine {
private static final String TAG = DucatusEngine.class.getSimpleName();
public BtcData coinData = null;
@ -48,7 +56,7 @@ public class DucatusEngine extends BtcEngine {
} else if (context.getCoinData() instanceof BtcData) {
coinData = (BtcData) context.getCoinData();
} else {
throw new Exception("Invalid type of Blockchain data for LtcEngine");
throw new Exception("Invalid type of Blockchain data for DucatusEngine");
}
}
@ -83,7 +91,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getBalanceCurrency() {
return "LTC";
return "DUC";
}
@Override
@ -118,7 +126,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getFeeCurrency() {
return "LTC";
return "DUC";
}
@Override
@ -168,16 +176,12 @@ public class DucatusEngine extends BtcEngine {
@Override
public Uri getWalletExplorerUri() {
return Uri.parse("https://live.blockcypher.com/ltc/address/" + ctx.getCoinData().getWallet());
return Uri.parse("https://insight.ducatus.io/#/DUC/mainnet/address/" + ctx.getCoinData().getWallet());
}
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
} else {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet());
}
return Uri.parse(ctx.getCoinData().getWallet());
}
@Override
@ -266,11 +270,11 @@ public class DucatusEngine extends BtcEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -317,7 +321,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException {
byte netSelectionByte = (byte) 0x30;
byte netSelectionByte = (byte) 0x31;
byte hash1[] = Util.calculateSHA256(pkUncompressed);
byte hash2[] = Util.calculateRIPEMD160(hash1);
@ -381,23 +385,28 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
final ArrayList<UnspentOutputInfo> unspentOutputs;
ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
checkBlockchainDataExists();
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
// Build script for our address
List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
// Collect unspent
unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) {
unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null));
}
long fullAmount = 0;
for (int i = 0; i < unspentOutputs.size(); ++i) {
@ -413,8 +422,8 @@ public class DucatusEngine extends BtcEngine {
change = change - fees;
}
final long amountFinal=amount;
final long changeFinal=change;
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));
@ -422,7 +431,7 @@ public class DucatusEngine extends BtcEngine {
final byte[][] txForSign = new byte[unspentOutputs.size()][];
final byte[][] bodyDoubleHash = new byte[unspentOutputs.size()][];
final byte[][] bodyHash= new byte[unspentOutputs.size()][];
final byte[][] bodyHash = new byte[unspentOutputs.size()][];
for (int i = 0; i < unspentOutputs.size(); ++i) {
txForSign[i] = BTCUtils.buildTXForSign(myAddress, targetAddress, myAddress, unspentOutputs, i, amount, change);
@ -434,13 +443,14 @@ public class DucatusEngine extends BtcEngine {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod==TangemCard.SigningMethod.Sign_Hash || signingMethod==TangemCard.SigningMethod.Sign_Raw;
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw;
}
@Override
public byte[][] getHashesToSign() throws Exception {
byte[][] dataForSign=new byte[unspentOutputs.size()][];
if (txForSign.length > 10) throw new Exception("To much hashes in one transaction!");
byte[][] dataForSign = new byte[unspentOutputs.size()][];
if (txForSign.length > 10)
throw new Exception("To much hashes in one transaction!");
for (int i = 0; i < unspentOutputs.size(); ++i) {
dataForSign[i] = bodyDoubleHash[i];
}
@ -479,7 +489,7 @@ public class DucatusEngine extends BtcEngine {
unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDer(r, s, pbKey);
}
byte[] txForSend=BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal);
byte[] txForSend = BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal);
notifyOnNeedSendTransaction(txForSend);
return txForSend;
}
@ -488,92 +498,43 @@ public class DucatusEngine extends BtcEngine {
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiInsight serverApiInsight = new ServerApiInsight();
ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
SingleObserver<BitcoreBalanceAndUnspents> balanceAndUnspentsObserver = new DisposableSingleObserver<BitcoreBalanceAndUnspents>() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
switch (method) {
case ServerApiInsight.INSIGHT_ADDRESS: {
try {
String walletAddress = insightResponse.getAddrStr();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(insightResponse.getBalanceSat());
coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat());
coinData.setValidationNodeDescription(ServerApiInsight.lastNode);
}
catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception");
}
public void onSuccess(BitcoreBalanceAndUnspents balanceAndUnspents) {
try {
coinData.setBalanceConfirmed(balanceAndUnspents.getBalance().getConfirmed());
coinData.setBalanceUnconfirmed(balanceAndUnspents.getBalance().getUnconfirmed());
coinData.setBalanceReceived(true);
for (BitcoreUtxo utxo : balanceAndUnspents.getUnspents()) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getMintTxid();
trUnspent.amount = utxo.getValue();
trUnspent.outputN = utxo.getMintIndex();
trUnspent.script = utxo.getScript();
coinData.getUnspentTransactions().add(trUnspent);
}
break;
blockchainRequestsCallbacks.onComplete(true);
case ServerApiInsight.INSIGHT_TRANSACTION: {
try {
String raw = insightResponse.getRawtx();
String txHash = new String(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(raw)))); //TODO: check
for (BtcData.UnspentTransaction tx : coinData.getUnspentTransactions()) {
if (tx.txID.equals(txHash))
tx.script = raw;
}
} catch (Exception e) {
e.printStackTrace();
}
}
break;
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
public void onSuccess(String method, List<InsightResponse> utxoList) {
// case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method
try {
coinData.getUnspentTransactions().clear();
for (InsightResponse utxo : utxoList) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTxid();
trUnspent.amount = utxo.getSatoshis();
trUnspent.outputN = utxo.getHeight();
coinData.getUnspentTransactions().add(trUnspent);
}
for (InsightResponse utxo : utxoList) {
//if (height != -1) { TODO: check
if (blockchainRequestsCallbacks.allowAdvance()) {
serverApiInsight.requestData(ServerApiInsight.INSIGHT_TRANSACTION, "", utxo.getTxid());
} else {
ctx.setError("Terminated by user");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) { //TODO: rework request sequence
ctx.setError(message);
} catch (Exception e) {
Log.e(TAG, "FAIL BITCORE_BALANCE_AND_UNSPENTS Exception");
e.printStackTrace();
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "FAIL BITCORE_BALANCE_AND_UNSPENTS Exception");
e.printStackTrace();
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_ADDRESS, coinData.getWallet(), "");
serverApiInsight.requestData(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS, coinData.getWallet(), "");
ServerApiBitcore serverApiBitcore = new ServerApiBitcore();
serverApiBitcore.getBalanceAndUnspents(coinData.getWallet(), balanceAndUnspentsObserver);
}
// private final static BigDecimal relayFee = new BigDecimal(0.00001);
@ -582,114 +543,38 @@ public class DucatusEngine extends BtcEngine {
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
coinData.minFee=null;
coinData.maxFee=null;
coinData.normalFee=null;
final ServerApiInsight serverApiInsight = new ServerApiInsight();
coinData.minFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000089)), ctx.getBlockchain().getCurrency()); //fee for byte from Ducatus wallet for android
coinData.normalFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000144)), ctx.getBlockchain().getCurrency());
coinData.maxFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000350)), ctx.getBlockchain().getCurrency());
final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
if ( method.equals(ServerApiInsight.INSIGHT_FEE)) {
try {
BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB
BigDecimal normalFee = new BigDecimal(insightResponse.getFee3());
BigDecimal maxFee = new BigDecimal(insightResponse.getFee6());
if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) {
serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "","");
}
minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size
normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network
// if (fee.compareTo(relayFee) < 0) {
// fee = relayFee;
// }
minFee = minFee.setScale(8, RoundingMode.DOWN);
normalFee = normalFee.setScale(8, RoundingMode.DOWN);
maxFee = maxFee.setScale(8, RoundingMode.DOWN);
coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency());
coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency());
coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency());
blockchainRequestsCallbacks.onComplete(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onSuccess (String method, List<InsightResponse> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) {
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", "");
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) {
final ServerApiInsight serverApiInsight = new ServerApiInsight();
final String txStr = BTCUtils.toHex(txForSend);
final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
SingleObserver<BitcoreSendResponse> sendResponseObserver = new DisposableSingleObserver<BitcoreSendResponse>() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
if (method.equals(ServerApiInsight.INSIGHT_SEND)) {
String resultString = insightResponse.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, List<InsightResponse> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) {
ctx.setError(message);
public void onSuccess(BitcoreSendResponse sendResponse) {
if (sendResponse.getTxid() != null) {
blockchainRequestsCallbacks.onComplete(true);
} else {
ctx.setError("Unknown send error");
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_SEND, "", txStr);
@Override
public void onError(Throwable e) {
Log.e(TAG, "onError: Bitcore sendTransaction" + e.getMessage());
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
ServerApiBitcore serverApiBitcore = new ServerApiBitcore();
serverApiBitcore.sendTransaction(txStr, sendResponseObserver);
}
}

View file

@ -293,11 +293,11 @@ public class EosEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
// }
return true;

View file

@ -334,11 +334,11 @@ public class EthEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
// }
return true;

View file

@ -0,0 +1,68 @@
package com.tangem.wallet.ethID;
import android.os.Bundle;
import android.util.Log;
import com.tangem.wallet.CoinData;
public class EthIdData extends CoinData {
private boolean hasApprovalTx = false;
private Long approvalAddressNonce;
private byte[] CKDpub = null;
public boolean isHasApprovalTx() {
return hasApprovalTx;
}
public void setHasApprovalTx(boolean hasApprovalTx) {
this.hasApprovalTx = hasApprovalTx;
}
public long getApprovalAddressNonce() {
return approvalAddressNonce;
}
public void setApprovalAddressNonce(long approvalAddressNonce) {
this.approvalAddressNonce = approvalAddressNonce;
}
public byte[] getCKDpub() {
return CKDpub;
}
public void setCKDpub(byte[] CKDpub) {
this.CKDpub = CKDpub;
}
@Override
public void clearInfo() {
super.clearInfo();
hasApprovalTx = false;
approvalAddressNonce = null;
}
@Override
public void loadFromBundle(Bundle B) {
super.loadFromBundle(B);
if (B.containsKey("ApprovalAddressNonce")) {
approvalAddressNonce = B.getLong("ApprovalAddressNonce");
}
if (B.containsKey("CKDpub")) CKDpub = B.getByteArray("CKDpub");
hasApprovalTx = B.getBoolean("HasApprovalTx");
}
@Override
public void saveToBundle(Bundle B) {
super.saveToBundle(B);
try {
if (approvalAddressNonce != null) B.putLong("ApprovalAddressNonce", approvalAddressNonce);
if (CKDpub != null) B.putByteArray("CKDpub", CKDpub);
B.putBoolean("HasApprovalTx", hasApprovalTx);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
}
}

View file

@ -0,0 +1,534 @@
package com.tangem.wallet.ethID;
import android.net.Uri;
import android.text.InputFilter;
import android.util.Log;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiInfura;
import com.tangem.data.network.model.BlockcypherFee;
import com.tangem.data.network.model.BlockcypherResponse;
import com.tangem.data.network.model.BlockcypherTx;
import com.tangem.data.network.model.BlockcypherTxref;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
import com.tangem.tangem_card.util.Util;
import com.tangem.util.CryptoUtil;
import com.tangem.util.DecimalDigitsInputFilter;
import com.tangem.wallet.BTCUtils;
import com.tangem.wallet.BalanceValidator;
import com.tangem.wallet.CoinData;
import com.tangem.wallet.CoinEngine;
import com.tangem.wallet.ECDSASignatureETH;
import com.tangem.wallet.EthTransaction;
import com.tangem.wallet.Keccak256;
import com.tangem.wallet.R;
import com.tangem.wallet.TangemContext;
import com.tangem.wallet.eth.EthData;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.SignatureDecodeException;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
public class EthIdEngine extends CoinEngine {
private static final String TAG = com.tangem.wallet.eth.EthEngine.class.getSimpleName();
public EthIdData coinData = null;
private byte[] approvalPubKey = Util.hexToBytes("04EAD74FEEE4061044F46B19EB654CEEE981E9318F0C8FE99AF5CDB9D779D2E52BB51EA2D14545E0B323F7A90CF4CC72753C973149009C10DB2D83DCEC28487729"); //TODO
public String approvalAddress = calculateAddress(approvalPubKey); //TODO
public EthIdEngine(TangemContext ctx) throws Exception {
super(ctx);
if (ctx.getCoinData() == null) {
coinData = new EthIdData();
ctx.setCoinData(coinData);
} else if (ctx.getCoinData() instanceof EthIdData) {
coinData = (EthIdData) ctx.getCoinData();
} else {
throw new Exception("Invalid type of Blockchain data for " + this.getClass().getSimpleName());
}
}
public EthIdEngine() {
super();
}
private static int getDecimals() {
return 18;
}
private int getChainIdNum() {
return EthTransaction.ChainEnum.Mainnet.getValue();
}
@Override
public boolean awaitingConfirmation() {
return false;
}
@Override
public Amount getBalance() {
return null;
}
@Override
public String getBalanceHTML() {
if (coinData != null) {
if (coinData.isHasApprovalTx()) {
return "VERIFIED";
} else {
return "NOT REGISTERED";
}
} else {
return "NOT REGISTERED";
}
}
@Override
public String getBalanceCurrency() {
return Blockchain.Ethereum.getCurrency();
}
@Override
public boolean isBalanceNotZero() {
return true;
}
@Override
public String getFeeCurrency() {
return Blockchain.Ethereum.getCurrency();
}
public boolean isNeedCheckNode() {
return false;
}
@Override
public CoinData createCoinData() {
return new EthIdData();
}
@Override
public String getUnspentInputsDescription() {
return "";
}
@Override
public boolean validateAddress(String address) {
if (address == null || address.isEmpty()) {
return false;
}
if (!address.startsWith("0x") && !address.startsWith("0X")) {
return false;
}
if (address.length() != 42) {
return false;
}
return true;
}
@Override
public String getBalanceEquivalent() {
Amount balance = getBalance();
if (balance == null) return "";
return balance.toEquivalentString(coinData.getRate());
}
@Override
public Amount convertToAmount(InternalAmount internalAmount) {
BigDecimal d = internalAmount.divide(new BigDecimal("1000000000000000000"), getDecimals(), RoundingMode.DOWN);
return new Amount(d, ctx.getBlockchain().getCurrency());
}
@Override
public Amount convertToAmount(String strAmount, String currency) {
return new Amount(strAmount, currency);
}
@Override
public InternalAmount convertToInternalAmount(Amount amount) {
return new InternalAmount(amount.multiply(new BigDecimal("1000000000000000000")), "wei");
}
@Override
public InternalAmount convertToInternalAmount(byte[] bytes) {
//throw new Exception("Not implemented");
return null;
}
@Override
public byte[] convertToByteArray(InternalAmount amount) throws Exception {
throw new Exception("Not implemented");
}
@Override
public boolean hasBalanceInfo() {
return true;
}
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
} else {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());
}
}
@Override
public Uri getWalletExplorerUri() {
if (ctx.getBlockchain() == Blockchain.EthereumTestNet)
return Uri.parse("https://rinkeby.etherscan.io/address/" + ctx.getCoinData().getWallet());
else
return Uri.parse("https://etherscan.io/address/" + ctx.getCoinData().getWallet());
}
@Override
public boolean isExtractPossible() {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data);
} else if (!isBalanceNotZero()) {
ctx.setMessage(R.string.general_wallet_empty);
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait);
} else {
return true;
}
return false;
}
@Override
public InputFilter[] getAmountInputFilters() {
return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())};
}
@Override
public boolean checkNewTransactionAmount(Amount amount) {
return true;
}
@Override
public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) {
return true;
}
@Override
public boolean validateBalance(BalanceValidator balanceValidator) {
if (coinData != null && coinData.isHasApprovalTx()) {
balanceValidator.setScore(100);
balanceValidator.setFirstLine(R.string.id_verified);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance);
return false;
} else {
balanceValidator.setScore(0);
balanceValidator.setFirstLine(R.string.id_not_registered);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance);
return false;
}
}
@Override
public String evaluateFeeEquivalent(String fee) {
try {
Amount feeValue = new Amount(fee, ctx.getBlockchain().getCurrency());
return feeValue.toEquivalentString(coinData.getRate());
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
@Override
public String calculateAddress(byte[] pkUncompressed) {
Keccak256 kec = new Keccak256();
int lenPk = pkUncompressed.length;
if (lenPk < 2) {
throw new IllegalArgumentException("Uncompress public key length is invalid");
}
byte[] cleanKey = new byte[lenPk - 1];
for (int i = 0; i < cleanKey.length; ++i) {
cleanKey[i] = pkUncompressed[i + 1];
}
byte[] r = kec.digest(cleanKey);
byte[] address = new byte[20];
for (int i = 0; i < 20; ++i) {
address[i] = r[i + 12];
}
return String.format("0x%s", BTCUtils.toHex(address));
}
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) {
final EthTransaction tx = constructIdTxForSign(coinData.getApprovalAddressNonce());
return new SignTask.TransactionToSign() {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod == TangemCard.SigningMethod.Sign_Hash;
}
@Override
public byte[][] getHashesToSign() {
byte[][] hashesForSign = new byte[1][];
hashesForSign[0] = tx.getRawHash();
return hashesForSign;
}
@Override
public byte[] getRawDataToSign() throws Exception {
throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName());
}
@Override
public String getHashAlgToSign() throws Exception {
throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName());
}
@Override
public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception {
throw new Exception("Transaction validation by issuer not supported in this version");
}
@Override
public byte[] onSignCompleted(byte[] signFromCard) throws Exception {
byte[] for_hash = tx.getRawHash();
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64));
s = CryptoUtil.toCanonicalised(s);
boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), approvalPubKey);
if (!f) {
Log.e(this.getClass().getSimpleName() + "-CHECK", "sign Failed.");
}
tx.signature = new ECDSASignatureETH(r, s);
int v = tx.BruteRecoveryID2(tx.signature, for_hash, approvalPubKey);
if (v != 27 && v != 28) {
Log.e(this.getClass().getSimpleName(), "invalid v");
throw new Exception("Error in " + this.getClass().getSimpleName() + " - invalid v");
}
tx.signature.v = (byte) v;
Log.e(this.getClass().getSimpleName(), " V: " + String.valueOf(v));
byte[] txForSend = tx.getEncoded();
notifyOnNeedSendTransaction(txForSend);
return txForSend;
}
};
}
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() {
@Override
public void onSuccess(String method, BlockcypherResponse blockcypherResponse) {
Log.i(TAG, "onSuccess: " + method);
try {
//TODO: change request logic, 2000 tx max
if (blockcypherResponse.getTxrefs() != null) {
for (BlockcypherTxref txref : blockcypherResponse.getTxrefs()) {
serverApiBlockcypher.requestData("ETH", ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txref.getTx_hash());
}
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError());
} else {
blockchainRequestsCallbacks.onProgress();
}
} else {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
}
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
}
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
}
};
ServerApiBlockcypher.TxResponseListener txListener = new ServerApiBlockcypher.TxResponseListener() {
@Override
public void onSuccess(BlockcypherTx response) {
Log.i(TAG, "onSuccess: BlockcypherTx");
try {
String address = approvalAddress;
if (address.startsWith("0x") || address.startsWith("0X")) {
address = address.substring(2);
}
if (response.getAddesses().contains(address)) {
coinData.setHasApprovalTx(true);
}
} catch (Exception e) {
Log.e(TAG, "onFail: BlockcypherTx" + response);
}
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onFail(String message) {
Log.i(TAG, "onFail: BlockcypherTx " + message);
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
};
serverApiBlockcypher.setResponseListener(blockcypherListener);
serverApiBlockcypher.setTxResponseListener(txListener);
serverApiBlockcypher.requestData("ETH", ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
requestApprovalAddressNonce();
}
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) {
String txStr = String.format("0x%s", BTCUtils.toHex(txForSend));
ServerApiInfura serverApiInfura = new ServerApiInfura();
// request requestData eth gasPrice listener
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) {
if (infuraResponse.getResult() == null || infuraResponse.getResult().isEmpty()) {
ctx.setError("Rejected by node: " + infuraResponse.getError());
blockchainRequestsCallbacks.onComplete(false);
} else {
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}
}
}
@Override
public void onFail(String method, String message) {
if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) {
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInfura.setResponseListener(responseListener);
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr);
}
public int pendingTransactionTimeoutInSeconds() {
return 10;
}
@Override
public boolean needMultipleLinesForBalance() {
return true;
}
@Override
public void defineWallet() throws CardProtocol.TangemException {
try {
String wallet = calculateAddress(calculateCKDpub(ctx.getCard().getIdHash()));
ctx.getCoinData().setWallet(wallet);
} catch (Exception e) {
ctx.getCoinData().setWallet("ERROR");
throw new CardProtocol.TangemException("Can't define wallet address");
}
}
private byte[] calculateCKDpub(byte[] idHash) {
DeterministicKey masterPubKey = HDKeyDerivation.createMasterPubKeyFromBytes(ctx.getCard().getWalletPublicKeyRar(), idHash);
DeterministicKey childPubKey = HDKeyDerivation.deriveChildKey(masterPubKey, new ChildNumber(1, false));
return childPubKey.getPubKey();
}
private EthTransaction constructIdTxForSign(long nonce) {
BigInteger nonceValue = BigInteger.valueOf(nonce);
BigInteger gasPrice = BigInteger.valueOf(10000000000L);
BigInteger gasLimit = BigInteger.valueOf(21000);
BigInteger weiAmount = gasPrice.multiply(gasLimit).add(BigInteger.ONE);
Integer chainId = this.getChainIdNum();
String to = coinData.getWallet();
if (to.startsWith("0x") || to.startsWith("0X")) {
to = to.substring(2);
}
return EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
}
private boolean checkSignature(byte[] signature, long nonce) throws SignatureDecodeException {
byte[] hashForSign = constructIdTxForSign(nonce).getRawHash();
// new Secp256k1Context();
return ECKey.verify(hashForSign, signature, coinData.getCKDpub());
}
private void requestApprovalAddressNonce() {
final ServerApiInfura serverApiInfura = new ServerApiInfura();
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
String pending = infuraResponse.getResult();
pending = pending.substring(2);
// BigInteger count = new BigInteger(pending, 16);
Long count = Long.valueOf(pending, 16);
coinData.setApprovalAddressNonce(count);
}
@Override
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
}
};
serverApiInfura.setResponseListener(responseListener);
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, approvalAddress, "", "");
}
}

View file

@ -31,6 +31,7 @@ import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONException;
@ -274,11 +275,11 @@ public class LtcEngine extends BtcEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -389,7 +390,15 @@ public class LtcEngine extends BtcEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -0,0 +1,49 @@
package com.tangem.wallet.tezos
import android.os.Bundle
import android.util.Log
import com.tangem.wallet.CoinData
import java.lang.Exception
class TezosData : CoinData() {
var balance: Long? = null
var counter: Long? = null
var publicKeyReavealed: Boolean? = null
var tezosPublicKey: String? = null
override fun clearInfo() {
super.clearInfo()
balance = null
counter = null
publicKeyReavealed = null
tezosPublicKey = null
}
override fun loadFromBundle(B: Bundle) {
super.loadFromBundle(B)
if (B.containsKey("Balance")) balance = B.getLong("Balance")
if (B.containsKey("Counter")) counter = B.getLong("Counter")
if (B.containsKey("PublicKeyReavealed")) publicKeyReavealed = B.getBoolean("PublicKeyReavealed")
if (B.containsKey("TezosPublicKey")) tezosPublicKey = B.getString("TezosPublicKey")
}
override fun saveToBundle(B: Bundle) {
super.saveToBundle(B)
try {
if (balance != null) B.putLong("Balance", balance!!)
if (counter != null) B.putLong("Counter", counter!!)
if (publicKeyReavealed != null) B.putBoolean("PublicKeyReavealed", publicKeyReavealed!!)
if (tezosPublicKey != null) B.putString("TezosPublicKey", tezosPublicKey!!)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}
}
}

View file

@ -0,0 +1,506 @@
package com.tangem.wallet.tezos
import android.net.Uri
import android.os.StrictMode
import android.text.InputFilter
import android.util.Log
import com.tangem.App
import com.tangem.data.network.ServerApiTezos
import com.tangem.data.network.model.TezosAccountResponse
import com.tangem.data.network.model.TezosForgeBody
import com.tangem.data.network.model.TezosOperationContent
import com.tangem.data.network.model.TezosPreapplyBody
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.util.CryptoUtil
import com.tangem.util.DecimalDigitsInputFilter
import com.tangem.wallet.*
import io.reactivex.observers.DisposableSingleObserver
import org.spongycastle.jcajce.provider.digest.Blake2b
import java.math.BigDecimal
class TezosEngine : CoinEngine {
constructor()
constructor(context: TangemContext) : super(context) {
if (context.coinData == null) {
coinData = TezosData()
context.coinData = coinData
} else if (context.coinData is TezosData) {
coinData = context.coinData as TezosData
} else {
throw Exception("Invalid type of Blockchain data for XlmEngine")
}
}
private val TAG = TezosEngine::class.java.simpleName
var coinData: TezosData? = null
private fun getDecimals() = 6
@Throws(Exception::class)
private fun checkBlockchainDataExists() {
if (coinData == null) throw Exception("No blockchain data")
}
override fun awaitingConfirmation(): Boolean {
return App.pendingTransactionsStorage.hasTransactions(ctx.card)
}
override fun getBalanceHTML(): String? {
val balance = balance
return if (balance != null) {
balance.toDescriptionString(getDecimals())
} else {
""
}
}
override fun getBalanceCurrency(): String? {
return "XTZ"
}
override fun isBalanceNotZero(): Boolean {
if (coinData == null) return false
return if (balance == null) false else balance!!.notZero()
}
override fun hasBalanceInfo(): Boolean {
return if (coinData == null) false else coinData!!.balance != null
}
override fun isExtractPossible(): Boolean {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data)
} else if (!isBalanceNotZero) {
ctx.setMessage(R.string.general_wallet_empty)
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait)
} else {
return true
}
return false
}
override fun getFeeCurrency(): String? {
return "XTZ"
}
override fun validateAddress(address: String?): Boolean {
val prefixedHashWithChecksum = Base58.decodeBase58(address)
if (prefixedHashWithChecksum == null || prefixedHashWithChecksum.size != 27) return false
val prefixedHash = prefixedHashWithChecksum.copyOf(23)
val checksum = prefixedHashWithChecksum.copyOfRange(23, 27)
val calcChecksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4)
return calcChecksum.contentEquals(checksum)
}
override fun isNeedCheckNode(): Boolean {
return false
}
override fun getWalletExplorerUri(): Uri? {
return Uri.parse("https://tezblock.io/account/" + ctx.coinData.wallet)
}
override fun getShareWalletUri(): Uri? {
return Uri.parse(ctx.coinData.wallet)
}
override fun getAmountInputFilters(): Array<InputFilter>? {
return arrayOf(DecimalDigitsInputFilter(getDecimals()))
}
override fun checkNewTransactionAmount(amount: Amount): Boolean {
if (coinData == null) return false
return amount <= balance
}
override fun checkNewTransactionAmountAndFee(amountValue: Amount?,
feeValue: Amount?,
isIncludeFee: Boolean
): Boolean {
try {
checkBlockchainDataExists()
} catch (e: java.lang.Exception) {
e.printStackTrace()
return false
}
if (feeValue == null || amountValue == null) return false
if (feeValue.isZero || amountValue.isZero) return false
if (isIncludeFee && (amountValue > balance || amountValue < feeValue)) return false
if (!isIncludeFee && amountValue.add(feeValue) > balance) return false
return true
}
override fun validateBalance(balanceValidator: BalanceValidator): Boolean {
try {
if (ctx.card.offlineBalance == null &&
!ctx.coinData.isBalanceReceived ||
!ctx.coinData.isBalanceReceived &&
ctx.card.remainingSignatures != ctx.card.maxSignatures
) {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance)
return false
}
if (coinData!!.isBalanceReceived) {
balanceValidator.setScore(100)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance
balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain)
if (balance!!.isZero) {
balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet
balanceValidator.setSecondLine(R.string.empty_string)
}
}
// if (ctx.card.offlineBalance != null &&
// !coinData!!.isBalanceReceived &&
// ctx.card.remainingSignatures ==
// ctx.card.maxSignatures
// ) {
// balanceValidator.setScore(80)
// balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline
// balanceValidator.setSecondLine(
// R.string.balance_validator_second_line_internet_to_get_balance
// )
// }
return true
} catch (e: java.lang.Exception) {
e.printStackTrace()
return false
}
}
override fun getBalance(): Amount? {
var balanceAmount: Amount? = null
if (hasBalanceInfo()) {
val xtzBalance = BigDecimal
.valueOf(coinData!!.balance!!).movePointLeft(getDecimals())
balanceAmount = Amount(xtzBalance, balanceCurrency)
}
return balanceAmount
}
override fun evaluateFeeEquivalent(fee: String?): String? {
return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try {
val feeAmount = Amount(fee, feeCurrency)
feeAmount.toEquivalentString(coinData!!.rate.toDouble())
} catch (e: java.lang.Exception) {
""
}
}
override fun getBalanceEquivalent(): String? {
if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return ""
val balance = balance ?: return ""
return balance.toEquivalentString(coinData!!.rate.toDouble())
}
override fun calculateAddress(pkUncompressed: ByteArray): String? {
val publicKeyHash = Blake2b.Blake2b160().digest(pkUncompressed)
val tz1Prefix = Util.hexToBytes("06A19F")
val prefixedHash = tz1Prefix + publicKeyHash
val checksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4)
val prefixedHashWithChecksum = prefixedHash + checksum
return Base58.encodeBase58(prefixedHashWithChecksum)
}
fun calculateTezosPublicKey(pkUncompressed: ByteArray): String {
val edpkPrefix = Util.hexToBytes("0D0F25D9")
val prefixedPubKey = edpkPrefix + pkUncompressed
val checksum = CryptoUtil.doubleSha256(prefixedPubKey).copyOfRange(0, 4)
val prefixedHashWithChecksum = prefixedPubKey + checksum
return Base58.encodeBase58(prefixedHashWithChecksum)
}
override fun convertToAmount(internalAmount: InternalAmount): Amount {
return Amount(internalAmount.movePointLeft(getDecimals()), balanceCurrency)
}
override fun convertToAmount(strAmount: String, currency: String): Amount {
return Amount(strAmount, currency)
}
override fun convertToInternalAmount(amount: Amount): InternalAmount {
return InternalAmount(amount.movePointRight(getDecimals()), "mutez")
}
override fun convertToInternalAmount(bytes: ByteArray?): InternalAmount? {
if (bytes == null) return null
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return InternalAmount(Util.byteArrayToLong(reversed), "mutez")
}
override fun convertToByteArray(internalAmount: InternalAmount): ByteArray? {
val bytes = Util.longToByteArray(internalAmount.longValueExact())
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return reversed
}
override fun createCoinData(): CoinData {
return TezosData()
}
override fun getUnspentInputsDescription() = ""
override fun constructTransaction(
amountValue: Amount,
feeValue: Amount,
IncFee: Boolean,
targetAddress: String
): SignTask.TransactionToSign {
checkBlockchainDataExists()
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
val finalAmount = if (IncFee) {
amountValue.minus(feeValue)
} else {
amountValue
}
val serverApiTezos = ServerApiTezos()
val headerResponse = serverApiTezos.header
val contents = arrayListOf<TezosOperationContent>()
var counter = coinData!!.counter!!
if (!coinData!!.publicKeyReavealed!!) {
counter++
val revealOp = TezosOperationContent(
kind = "reveal",
source = coinData!!.wallet,
fee = "1300",
counter = counter.toString(),
gas_limit = "10000",
storage_limit = "0",
public_key = coinData!!.tezosPublicKey!!
)
contents.add(revealOp)
}
counter++
val transactionOp = TezosOperationContent(
kind = "transaction",
source = coinData!!.wallet,
fee = "1350",
counter = counter.toString(),
gas_limit = "10600",
storage_limit = "277",
destination = targetAddress,
amount = finalAmount.movePointRight(getDecimals()).toBigInteger().toString()
)
contents.add(transactionOp)
val tezosForgeBody = TezosForgeBody(headerResponse.hash!!, contents)
val forgeResponse = serverApiTezos.forgeOperations(tezosForgeBody)
val watermark = "03"
val forgedBytes = Util.hexToBytes(watermark + forgeResponse)
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash
}
@Throws(java.lang.Exception::class)
override fun getHashesToSign(): Array<ByteArray?> {
val dataForSign = arrayOfNulls<ByteArray>(1)
dataForSign[0] = Blake2b.Blake2b256().digest(forgedBytes)
return dataForSign
}
@Throws(java.lang.Exception::class)
override fun getRawDataToSign(): ByteArray? {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
@Throws(java.lang.Exception::class)
override fun getHashAlgToSign(): String? {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
@Throws(java.lang.Exception::class)
override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray?): ByteArray? {
throw java.lang.Exception("Transaction validation by issuer not supported in this version")
}
@Throws(java.lang.Exception::class)
override fun onSignCompleted(signFromCard: ByteArray): ByteArray? {
val edsigPrefix = Util.hexToBytes("09F5CD8612")
val prefixedSignature = edsigPrefix + signFromCard
val checksum = CryptoUtil.doubleSha256(prefixedSignature).copyOfRange(0, 4)
val prefixedSignatureWithChecksum = prefixedSignature + checksum
val preapplyBody = TezosPreapplyBody(
protocol = headerResponse.protocol!!,
branch = headerResponse.hash!!,
contents = contents,
signature = Base58.encodeBase58(prefixedSignatureWithChecksum)
)
try {
serverApiTezos.peapplyOperations(preapplyBody)
} catch (e: java.lang.Exception) {
ctx.error = e.message
return null
}
val txForSend = Util.hexToBytes(forgeResponse) + signFromCard
notifyOnNeedSendTransaction(txForSend)
return txForSend
}
}
}
override fun requestBalanceAndUnspentTransactions(
blockchainRequestsCallbacks: BlockchainRequestsCallbacks
) {
coinData!!.tezosPublicKey = calculateTezosPublicKey(ctx.card.walletPublicKey)
val serverApiTezos = ServerApiTezos()
val accountObserver = object : DisposableSingleObserver<TezosAccountResponse>() {
override fun onSuccess(response: TezosAccountResponse) {
coinData!!.balance = response.balance
coinData!!.isBalanceReceived = true
coinData!!.counter = response.counter
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(ctx.hasError())
} else {
blockchainRequestsCallbacks.onProgress()
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "requestBalanceAndUnspentTransactions error " + e.message)
ctx.error = e.message
e.printStackTrace()
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(false)
} else {
blockchainRequestsCallbacks.onProgress()
}
}
}
val managerKeyObserver = object : DisposableSingleObserver<String>() {
override fun onSuccess(response: String) {
coinData!!.publicKeyReavealed = true
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(ctx.hasError())
} else {
blockchainRequestsCallbacks.onProgress()
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "requestFee error " + e.message)
e.printStackTrace()
coinData!!.publicKeyReavealed = false // error expected when key is not revealed
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(ctx.hasError())
} else {
blockchainRequestsCallbacks.onProgress()
}
}
}
serverApiTezos.getAddress(coinData!!.wallet, accountObserver)
serverApiTezos.getMangerKey(coinData!!.wallet, managerKeyObserver)
}
override fun requestFee(
blockchainRequestsCallbacks: BlockchainRequestsCallbacks,
targetAddress: String?,
amount: Amount?
) {
var fee: BigDecimal = BigDecimal.valueOf(0.00135)
if (!coinData!!.publicKeyReavealed!!) {
fee += BigDecimal.valueOf(0.0013)
}
val serverApiTezos = ServerApiTezos()
val accountObserver = object : DisposableSingleObserver<TezosAccountResponse>() {
override fun onSuccess(response: TezosAccountResponse) {
if (response.balance == 0L) {
fee += BigDecimal.valueOf(0.257)
}
val feeAmount = Amount(fee, feeCurrency)
coinData!!.minFee = feeAmount
coinData!!.normalFee = feeAmount
coinData!!.maxFee = feeAmount
blockchainRequestsCallbacks.onComplete(true)
}
override fun onError(e: Throwable) {
Log.e(TAG, "requestFee error " + e.message)
ctx.error = e.message
e.printStackTrace()
blockchainRequestsCallbacks.onComplete(false)
}
}
serverApiTezos.getAddress(targetAddress, accountObserver)
}
override fun requestSendTransaction(
blockchainRequestsCallbacks: BlockchainRequestsCallbacks,
txForSend: ByteArray?
) {
if (txForSend == null) {
blockchainRequestsCallbacks.onComplete(false)
} else {
val injectObserver = object : DisposableSingleObserver<Any>() {
override fun onSuccess(response: Any) {
blockchainRequestsCallbacks.onComplete(true)
}
override fun onError(e: Throwable) {
blockchainRequestsCallbacks.onComplete(false)
}
}
ServerApiTezos().injectOperations(Util.bytesToHex(txForSend), injectObserver)
}
}
override fun allowSelectFeeLevel(): Boolean {
return false
}
override fun pendingTransactionTimeoutInSeconds(): Int {
return 60
}
}

View file

@ -275,11 +275,11 @@ public class XlmAssetEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getXlmBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -243,11 +243,11 @@ public class XlmEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -0,0 +1,133 @@
package com.tangem.wallet.xlmTag
import android.os.Bundle
import android.util.Log
import com.tangem.wallet.CoinData
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngine.InternalAmount
import org.stellar.sdk.KeyPair
import org.stellar.sdk.responses.AccountResponse
import org.stellar.sdk.responses.LedgerResponse
import java.math.BigDecimal
class XlmTagData : CoinData() {
class AccountResponseEx internal constructor(accountId: String?, sequenceNumber: Long?) : AccountResponse(KeyPair.fromAccountId(accountId), sequenceNumber)
private var balance: CoinEngine.Amount? = null
private var sequenceNumber: Long? = 0L
private var baseReserve: CoinEngine.Amount? = CoinEngine.Amount("0.5", "XLM")
var baseFee: CoinEngine.Amount? = CoinEngine.Amount("0.00001", "XLM")
private set
var isError404 = false
var isTargetAccountCreated = false
var fundsFromTrustedSource = false
var fundsSentToTrustedSource = false
override fun clearInfo() {
super.clearInfo()
balance = null
isError404 = false
isTargetAccountCreated = false
fundsFromTrustedSource = false
fundsSentToTrustedSource = false
}
fun getBalance(): CoinEngine.Amount? {
return if (balance != null) {
CoinEngine.Amount(balance!!.subtract(reserve), "XLM")
} else {
null
}
}
val reserve: CoinEngine.Amount
get() = CoinEngine.Amount(baseReserve!!.multiply(BigDecimal.valueOf(2)), "XLM")
var accountResponse: AccountResponse
get() = XlmTagData.AccountResponseEx(wallet, sequenceNumber)
set(accountResponse) {
if (accountResponse.balances.size > 0) {
val balanceResponse = accountResponse.balances[0]
balance = CoinEngine.Amount(balanceResponse.balance, "XLM")
}
sequenceNumber = accountResponse.sequenceNumber
isBalanceReceived = true
}
fun setLedgerResponse(ledgerResponse: LedgerResponse) {
val xlmEngine = XlmTagEngine()
baseReserve = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseReserveInStroops, "stroops"))
baseFee = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseFeeInStroops, "stroops"))
}
fun incSequenceNumber() {
sequenceNumber = sequenceNumber?.inc()
}
override fun loadFromBundle(B: Bundle) {
super.loadFromBundle(B)
balance = if (B.containsKey("BalanceCurrency") && B.containsKey("BalanceDecimal")) {
CoinEngine.Amount(B.getString("BalanceDecimal"), B.getString("BalanceCurrency"))
} else {
null
}
sequenceNumber = if (B.containsKey("sequenceNumber")) {
B.getLong("sequenceNumber")
} else {
0L
}
baseReserve = if (B.containsKey("BaseReserveCurrency") && B.containsKey("BaseReserveDecimal")) {
CoinEngine.Amount(B.getString("BaseReserveDecimal"), B.getString("BaseReserveCurrency"))
} else {
CoinEngine.Amount("0.5", "XLM")
}
baseFee = if (B.containsKey("BaseFeeCurrency") && B.containsKey("BaseFeeDecimal")) {
CoinEngine.Amount(B.getString("BaseFeeDecimal"), B.getString("BaseFeeCurrency"))
} else {
CoinEngine.Amount("0.00001", "XLM")
}
if (B.containsKey("Error404")) isError404 = B.getBoolean("Error404") else isError404 = false
if (B.containsKey("TargetAccountCreated")) isTargetAccountCreated = B.getBoolean("TargetAccountCreated") else isTargetAccountCreated = false
fundsFromTrustedSource = if (B.containsKey("FundsFromTrustedSource")) {
B.getBoolean("FundsFromTrustedSource")
} else {
false
}
fundsSentToTrustedSource = if (B.containsKey("FundsSentToTrustedSource")) {
B.getBoolean("FundsSentToTrustedSource")
} else {
false
}
}
override fun saveToBundle(B: Bundle) {
super.saveToBundle(B)
try {
if (balance != null) {
B.putString("BalanceCurrency", balance!!.currency)
B.putString("BalanceDecimal", balance!!.toValueString())
}
if (sequenceNumber != null) {
B.putLong("sequenceNumber", sequenceNumber!!)
}
if (baseReserve != null) {
B.putString("BaseReserveCurrency", baseReserve!!.currency)
B.putString("BaseReserveDecimal", baseReserve!!.toValueString())
}
if (baseFee != null) {
B.putString("BaseFeeCurrency", baseFee!!.currency)
B.putString("BaseFeeDecimal", baseFee!!.toValueString())
}
if (isError404) B.putBoolean("Error404", true)
if (isTargetAccountCreated) B.putBoolean("TargetAccountCreated", true)
if (fundsFromTrustedSource) B.putBoolean("FundsFromTrustedSource", true)
if (fundsSentToTrustedSource) B.putBoolean("FundsSentToTrustedSource", true)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}
}
}

View file

@ -0,0 +1,423 @@
package com.tangem.wallet.xlmTag
import android.net.Uri
import android.os.StrictMode
import android.text.InputFilter
import android.util.Log
import com.tangem.App
import com.tangem.data.Blockchain
import com.tangem.data.network.ServerApiStellar
import com.tangem.data.network.StellarRequest
import com.tangem.data.network.StellarRequest.Ledgers
import com.tangem.data.network.StellarRequest.SubmitTransaction
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.util.DecimalDigitsInputFilter
import com.tangem.wallet.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.stellar.sdk.*
import org.stellar.sdk.requests.RequestBuilder
import org.stellar.sdk.responses.operations.OperationResponse
import org.stellar.sdk.responses.operations.PaymentOperationResponse
import java.io.IOException
import java.math.BigDecimal
class XlmTagEngine : CoinEngine {
var coinData: XlmTagData? = null
val operations = mutableListOf<OperationResponse>()
constructor(context: TangemContext) : super(context) {
if (context.coinData == null) {
coinData = XlmTagData()
context.coinData = coinData
} else if (context.coinData is XlmTagData) {
coinData = context.coinData as XlmTagData
} else {
throw Exception("Invalid type of Blockchain data for XlmEngine")
}
}
constructor() : super() {}
@Throws(Exception::class)
private fun checkBlockchainDataExists() {
if (coinData == null) throw Exception("No blockchain data")
}
override fun awaitingConfirmation(): Boolean {
return App.pendingTransactionsStorage.hasTransactions(ctx.card)
}
override fun getBalanceHTML(): String {
return if (hasBalanceInfo()) {
if (coinData!!.fundsFromTrustedSource) {
// if (coinData!!.fundsSentToTrustedSource) { //TODO: shut down for example tags to appear genuine, turn on for production
// ctx.getString(R.string.tag_claimed)
// } else {
ctx.getString(R.string.tag_genuine)
// }
} else {
ctx.getString(R.string.tag_not_genuine)
}
} else {
""
}
}
override fun getBalanceCurrency(): String {
return "XLM"
}
override fun isBalanceNotZero(): Boolean {
if (coinData == null) return false
return if (coinData!!.getBalance() == null) false else coinData!!.getBalance()!!.notZero()
}
override fun hasBalanceInfo(): Boolean {
return if (coinData == null) false else coinData!!.isBalanceReceived
}
override fun isExtractPossible(): Boolean {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data)
} else if (!isBalanceNotZero) {
ctx.setMessage(R.string.general_wallet_empty)
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait)
} else {
return true
}
return false
}
override fun getFeeCurrency(): String {
return "XLM"
}
override fun validateAddress(address: String): Boolean {
try {
val kp = KeyPair.fromAccountId(address)
} catch (e: Exception) {
return false
}
return true
}
override fun isNeedCheckNode(): Boolean {
return false
}
override fun getWalletExplorerUri(): Uri {
return Uri.parse("https://stellar.expert/explorer/public/account/" + ctx.coinData.wallet)
}
override fun getShareWalletUri(): Uri {
return if (ctx.card.denomination != null) {
Uri.parse(ctx.coinData.wallet + "?amount=" + convertToAmount(convertToInternalAmount(ctx.card.denomination)!!).toValueString())
} else {
Uri.parse(ctx.coinData.wallet)
}
}
override fun getAmountInputFilters(): Array<InputFilter> {
return arrayOf(DecimalDigitsInputFilter(decimals))
}
override fun checkNewTransactionAmount(amount: Amount): Boolean {
return true
}
override fun checkNewTransactionAmountAndFee(amountValue: Amount, feeValue: Amount, isIncludeFee: Boolean): Boolean {
return true
}
override fun validateBalance(balanceValidator: BalanceValidator): Boolean {
try {
if (!hasBalanceInfo()) {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_no_connection
balanceValidator.setSecondLine(R.string.balance_validator_second_line_authenticity_not_verified)
return false
}
if (coinData!!.fundsFromTrustedSource) {
balanceValidator.setScore(100)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_in_blockchain
balanceValidator.setSecondLine(R.string.empty_string)
} else {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_authenticity
balanceValidator.setSecondLine(R.string.empty_string)
}
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
override fun getBalance(): Amount? {
return if (!hasBalanceInfo()) null else coinData!!.getBalance()!!
}
override fun evaluateFeeEquivalent(fee: String): String {
return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try {
val feeAmount = Amount(fee, feeCurrency)
feeAmount.toEquivalentString(coinData!!.rate.toDouble())
} catch (e: Exception) {
""
}
}
override fun getBalanceEquivalent(): String {
if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return ""
val balance = balance ?: return ""
return balance.toEquivalentString(coinData!!.rate.toDouble())
}
override fun calculateAddress(pkUncompressed: ByteArray): String {
val kp = KeyPair.fromPublicKey(pkUncompressed)
return kp.accountId
}
override fun convertToAmount(internalAmount: InternalAmount): Amount {
val d = internalAmount.divide(multiplier)
return Amount(d, balanceCurrency)
}
override fun convertToAmount(strAmount: String, currency: String): Amount {
return Amount(strAmount, currency)
}
override fun convertToInternalAmount(amount: Amount): InternalAmount {
val d = amount.multiply(multiplier)
return InternalAmount(d, "stroops")
}
override fun convertToInternalAmount(bytes: ByteArray): InternalAmount? {
if (bytes == null) return null
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return InternalAmount(Util.byteArrayToLong(reversed), "stroops")
}
override fun convertToByteArray(internalAmount: InternalAmount): ByteArray {
val bytes = Util.longToByteArray(internalAmount.longValueExact())
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return reversed
}
override fun createCoinData(): CoinData {
return XlmTagData()
}
override fun getUnspentInputsDescription(): String {
return ""
}
@Throws(Exception::class)
override fun constructTransaction(amountValue: Amount, feeValue: Amount, IncFee: Boolean, targetAddress: String): SignTask.TransactionToSign {
var amountValue = amountValue
checkBlockchainDataExists()
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
if (IncFee) {
amountValue = Amount(amountValue.subtract(feeValue), amountValue.currency)
}
val operation: Operation
operation = if (coinData!!.isTargetAccountCreated) PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), AssetTypeNative(), amountValue.toValueString()).build() else CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build()
val transaction = TransactionEx.buildEx(60, coinData!!.accountResponse, operation)
if (transaction.fee != convertToInternalAmount(feeValue).intValueExact()) {
throw Exception("Invalid fee!")
}
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw
}
@Throws(Exception::class)
override fun getHashesToSign(): Array<ByteArray> {
val dataForSign = arrayOf(transaction.hash())
return dataForSign
}
@Throws(Exception::class)
override fun getRawDataToSign(): ByteArray {
return transaction.signatureBase()
}
override fun getHashAlgToSign(): String {
return "sha-256"
}
@Throws(Exception::class)
override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray {
throw Exception("Issuer validation not supported!")
}
@Throws(Exception::class)
override fun onSignCompleted(signFromCard: ByteArray): ByteArray { // Sign the transaction to prove you are actually the person sending it.
transaction.setSign(signFromCard)
val txForSend = transaction.toEnvelopeXdrBase64().toByteArray()
notifyOnNeedSendTransaction(txForSend)
return txForSend
}
}
}
private fun checkTargetAccountCreated(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) {
val serverApi = ServerApiStellar(ctx.blockchain)
val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener {
override fun onSuccess(request: StellarRequest.Base) {
coinData!!.isTargetAccountCreated = true
blockchainRequestsCallbacks.onComplete(true)
}
override fun onFail(request: StellarRequest.Base) {
Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error)
if (request.errorResponse != null && request.errorResponse.code == 404) {
coinData!!.isTargetAccountCreated = false
if (amount.compareTo(coinData!!.reserve) >= 0) {
blockchainRequestsCallbacks.onComplete(true)
} else {
ctx.setError(R.string.confirm_transaction_error_not_enough_xlm_for_create)
blockchainRequestsCallbacks.onComplete(false)
}
} else { // suppose account is created if anything goes wrong
coinData!!.isTargetAccountCreated = true
blockchainRequestsCallbacks.onComplete(true)
}
}
}
serverApi.setListener(listener)
serverApi.requestData(ctx, StellarRequest.Balance(targetAddress))
}
private fun requestPayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
val server = Server("https://horizon.stellar.org/")
val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet)
try {
var operationsPage = server.payments().forAccount(accountKeyPair).limit(200).order(RequestBuilder.Order.DESC).execute()
Log.e("Stellar", operationsPage.records.toString())
operations.addAll(operationsPage.records)
while (operations.size < OPERATIONS_LIMIT) {
operationsPage = operationsPage.getNextPage(server.httpClient)
operations.addAll(operationsPage.records)
Log.e("Stellar", operationsPage.records.toString())
Log.e("Stellar", "Operations downloaded: " + operationsPage.records.count())
Log.e("Stellar", "Operations overall: " + operations.count())
}
parsePayments(blockchainRequestsCallbacks)
CoroutineScope(Dispatchers.Main).launch { blockchainRequestsCallbacks.onComplete(!ctx.hasError()) }
} catch (e: Exception) {
if (e.message != null) {
ctx.error = e.message
blockchainRequestsCallbacks.onComplete(false)
} else {
ctx.error = e.javaClass.name
blockchainRequestsCallbacks.onComplete(false)
}
}
}
private fun parsePayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
coinData!!.fundsFromTrustedSource = operations.find { it is PaymentOperationResponse && it.from.accountId == TRUSTED_SOURCE } != null
coinData!!.fundsSentToTrustedSource =
(operations.find { it is PaymentOperationResponse && it.to.accountId == TRUSTED_DESTINATION } != null) && coinData!!.fundsFromTrustedSource
Log.e("Stellar",
"fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " +
"fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource")
coinData!!.isBalanceReceived = true
}
override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
CoroutineScope(Dispatchers.IO).launch { requestPayments(blockchainRequestsCallbacks) }
}
@Throws(Exception::class)
override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) {
coinData!!.maxFee = coinData!!.baseFee
coinData!!.normalFee = coinData!!.maxFee
coinData!!.minFee = coinData!!.normalFee
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount)
}
@Throws(IOException::class)
override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray) {
val serverApi = ServerApiStellar(ctx.blockchain)
val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener {
override fun onSuccess(request: StellarRequest.Base) {
try {
if (!SubmitTransaction::class.java.isInstance(request)) throw Exception("Invalid request logic")
val submitTransactionRequest = request as SubmitTransaction
if (submitTransactionRequest.response.isSuccess) {
ctx.error = null
blockchainRequestsCallbacks.onComplete(true)
} else {
if (submitTransactionRequest.response.extras != null && submitTransactionRequest.response.extras.resultCodes != null) {
var trResult = submitTransactionRequest.response.extras.resultCodes.transactionResultCode
if (submitTransactionRequest.response.extras.resultCodes.operationsResultCodes != null && submitTransactionRequest.response.extras.resultCodes.operationsResultCodes.size > 0) {
trResult += "/" + submitTransactionRequest.response.extras.resultCodes.operationsResultCodes[0]
}
ctx.error = trResult
} else {
ctx.error = "transaction failed"
}
blockchainRequestsCallbacks.onComplete(false)
}
} catch (e: Exception) {
if (e.message != null) {
ctx.error = e.message
blockchainRequestsCallbacks.onComplete(false)
} else {
ctx.error = e.javaClass.name
blockchainRequestsCallbacks.onComplete(false)
}
}
}
override fun onFail(request: StellarRequest.Base) {
ctx.error = request.error
blockchainRequestsCallbacks.onComplete(false)
}
}
serverApi.setListener(listener)
val transaction = TransactionEx.fromEnvelopeXdr(String(txForSend))
coinData!!.incSequenceNumber()
serverApi.requestData(ctx, SubmitTransaction(transaction))
}
override fun needMultipleLinesForBalance(): Boolean {
return true
}
override fun allowSelectFeeLevel(): Boolean {
return false
}
override fun pendingTransactionTimeoutInSeconds(): Int {
return 10
}
companion object {
private val TAG = XlmTagEngine::class.java.simpleName
private val decimals: Int
private get() = 7
private val multiplier = BigDecimal("10000000")
const val OPERATIONS_LIMIT = 200
private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM"
private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2"
}
}

View file

@ -228,11 +228,11 @@ public class XrpEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#505050"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,4L3,1h2v3h3v2L5,6v3L3,9L3,6L0,6L0,4h3zM6,10L6,7h3L9,4h7l1.83,2L21,6c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2L5,22c-1.1,0 -2,-0.9 -2,-2L3,10h3zM13,19c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5 -5,2.24 -5,5 2.24,5 5,5zM9.8,14c0,1.77 1.43,3.2 3.2,3.2s3.2,-1.43 3.2,-3.2 -1.43,-3.2 -3.2,-3.2 -3.2,1.43 -3.2,3.2z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorAccent"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

View file

@ -0,0 +1,4 @@
<vector android:height="100dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="100dp" android:tint="#505050" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorAccent"
android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
</vector>

View file

@ -198,7 +198,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:fontFamily="@font/maax"
android:text="The card is linked"
android:text="@string/details_linked_card_title"
android:textSize="@dimen/text_size_1_small" />
<TextView

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.otaliastudios.cameraview.CameraView
android:id="@+id/cvCamera"
android:layout_width="350dp"
android:layout_height="350dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
app:cameraAudio="off"
app:cameraFlash="auto"
app:cameraMode="picture"
app:cameraPictureSizeMaxHeight="200"
app:cameraPictureSizeMaxWidth="200"
app:cameraPictureSizeMinHeight="200"
app:cameraPictureSizeMinWidth="200"
app:cameraPictureSizeSmallest="true"
app:cameraRequestPermissions="true"
app:cameraSnapshotMaxHeight="200"
app:cameraSnapshotMaxWidth="200"
app:cameraUseDeviceOrientation="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/ivTakePicture"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_take_picture"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cvCamera" />
<ImageView
android:id="@+id/ivFlipCamera"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_flip_camera"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivTakePicture" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,254 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
android:padding="0dp"
tools:ignore="contentDescription">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/srl"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/rlAuxButtons"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="false"
android:background="@color/white"
android:orientation="vertical">
<include
android:id="@+id/loCard"
layout="@layout/layout_id" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toBottomOf="@+id/loCard">
<LinearLayout
android:id="@+id/loBalance"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center_vertical"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<TextView
android:id="@+id/tvBalanceLine1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:visibility="visible"
tools:textColor="@color/confirmed" />
<TextView
android:id="@+id/tvBalanceLine2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:fontFamily="@font/maax"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textAlignment="center"
android:visibility="visible" />
</LinearLayout>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.3" />
<LinearLayout
android:id="@+id/loIdentificationData"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:baselineAligned="false"
android:gravity="center_vertical|center_horizontal"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toBottomOf="@+id/guideline">
<TextView
android:id="@+id/tvDemoWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/card_state_empty"
android:textSize="14dp"
android:textStyle="normal"
android:text="@string/id_demo"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tvIdNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/primary_dark"
android:textSize="18dp"
android:textStyle="normal"
tools:text="ID # 203129301239" />
<TextView
android:id="@+id/tvFullName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/primary_dark"
android:textSize="28dp"
android:textStyle="normal"
tools:text="John Doe" />
<TextView
android:id="@+id/tvBirthDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:layout_marginBottom="5dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_1_small"
tools:text="2000-01-01" />
<TextView
android:id="@+id/tvGender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:layout_marginBottom="5dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="@dimen/text_size_1_small"
tools:text="Sex: M" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<RelativeLayout
android:id="@+id/rlMessageAndError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/rlAuxButtons">
<TextView
android:id="@+id/tvMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/msg_okay"
android:elevation="2dp"
android:fontFamily="@font/maax"
android:padding="4dp"
android:textAlignment="center"
android:textColor="@color/black"
android:textStyle="normal|bold"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/tvError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tvMessage"
android:layout_alignParentBottom="false"
android:background="@color/msg_err"
android:elevation="3dp"
android:fontFamily="@font/maax"
android:padding="4dp"
android:shadowColor="@color/cardview_shadow_star"
android:textAlignment="center"
android:textColor="@color/white"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout>
<LinearLayout
android:id="@+id/rlToolButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/rlAuxButtons"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnIssueNewId"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:enabled="true"
android:fontFamily="@font/saira_condensed_bold"
android:text="Issue New Id"
android:textSize="@dimen/text_size_large"/>
</LinearLayout>
<LinearLayout
android:id="@+id/rlAuxButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent">
<include layout="@layout/layout_btn_details" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnNewScan"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:backgroundTint="@color/white"
android:drawableStart="@drawable/ic_nfc_white_24dp"
android:fontFamily="@font/saira_condensed_bold"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:text="@string/loaded_wallet_btn_new_scan"
android:textColor="@color/primary_dark" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:padding="0dp"
tools:ignore="contentDescription">
<ImageView
android:id="@+id/ivPhoto"
app:layout_constraintHorizontal_bias=".46"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_add_a_photo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/etName"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:inputType="textCapWords"
android:hint="First Name"
android:layout_marginTop="24dp"
android:paddingBottom="16dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/ivPhoto"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<EditText
android:id="@+id/etSurname"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:inputType="textCapWords"
android:hint="Last Name"
android:paddingBottom="16dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/etName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/tvBirthDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:hint="Enter Date of Birth"
android:paddingTop="14dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/etSurname" />
<EditText
android:id="@+id/etBirthDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:hint="dd/mm/yyyy"
android:inputType="date"
android:paddingBottom="16dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/tvBirthDate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="@+id/tvSelectGender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:hint="Select Gender"
android:paddingTop="14dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textSize="18sp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/etBirthDate" />
<RadioGroup
android:id="@+id/rgGender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSelectGender">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/rbFemale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:text="Female"
android:textAlignment="center"
android:textSize="18sp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/rbMale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maaxmedium"
android:text="Male"
android:textAlignment="center"
android:textSize="18sp" />
</RadioGroup>
<LinearLayout
android:id="@+id/rlToolButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:fontFamily="@font/saira_condensed_bold"
android:text="Confirm"
android:textSize="@dimen/text_size_large" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,9 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tangem.ui.fragment.additional.TagFragment">
<include layout="@layout/fr_loaded_wallet"/>
</FrameLayout>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="Now touch the Issuer Key card"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="to confirm the new ID data"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="Now touch the ID card"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="@dimen/text_size_large"
tools:text="CB02 0000 0002 5000" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="to write the new ID data"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/loCard"
android:layout_width="match_parent"
android:layout_height="300dp"
android:maxHeight="300dp"
android:padding="3dp"
android:layout_marginTop="16dp"
app:layout_constraintHeight_max="350dp"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="contentDescription"
tools:showIn="@layout/fragment_id">
<ImageView
android:id="@+id/ivPhoto"
android:layout_width="300dp"
android:layout_height="300dp"
android:src="@drawable/id_card_default"
android:layout_centerInParent="true"/>
<RelativeLayout
android:id="@+id/rlProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:elevation="3dp"
android:padding="5dp"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.75"
android:background="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:alpha="1"
android:fontFamily="@font/maaxmedium"
android:text="@string/loaded_wallet_status_verifying"
android:textColor="@color/colorPrimary"
android:textSize="12dp"
android:textStyle="bold" />
<ProgressBar
android:layout_width="@dimen/size_progress_bar_big"
android:layout_height="@dimen/size_progress_bar_big"
android:layout_centerInParent="true"
android:alpha="1"
android:elevation="1dp"
android:progressTint="@color/colorPrimary" />
</RelativeLayout>
</RelativeLayout>

View file

@ -86,6 +86,7 @@
android:background="@color/white" />
<TextView
android:id="@+id/tvProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"

View file

@ -28,6 +28,12 @@
<action
android:id="@+id/action_main_to_settingsFragment"
app:destination="@id/settingsFragment" />
<action
android:id="@+id/action_main_to_tagFragment"
app:destination="@id/tagFragment" />
<action
android:id="@+id/action_main_to_idFragment"
app:destination="@id/idFragment" />
</fragment>
<fragment
@ -188,5 +194,48 @@
android:id="@+id/prepareKrakenWithdrawalFragment"
android:name="com.tangem.ui.fragment.additional.PrepareKrakenWithdrawalFragment"
android:label="PrepareKrakenWithdrawalFragment" />
<fragment
android:id="@+id/tagFragment"
android:name="com.tangem.ui.fragment.additional.TagFragment"
android:label="fragment_tag"
tools:layout="@layout/fragment_tag" />
<fragment
android:id="@+id/issueNewIdFragment"
android:name="com.tangem.ui.fragment.id.IssueNewIdFragment"
android:label="IssueNewIdFragment" >
<action
android:id="@+id/action_issueNewIdFragment_to_cameraFragment"
app:destination="@id/cameraFragment" />
<action
android:id="@+id/action_issueNewIdFragment_to_validateIdFragment"
app:destination="@id/validateIdFragment" />
</fragment>
<fragment
android:id="@+id/idFragment"
android:name="com.tangem.ui.fragment.id.IdFragment"
android:label="IdFragment" >
<action
android:id="@+id/action_idFragment_to_issueNewIdFragment"
app:destination="@id/issueNewIdFragment" />
<action
android:id="@+id/action_idFragment_to_verifyCard"
app:destination="@id/verifyCard" />
</fragment>
<fragment
android:id="@+id/cameraFragment"
android:name="com.tangem.ui.fragment.id.CameraFragment"
android:label="CameraFragment" />
<fragment
android:id="@+id/validateIdFragment"
android:name="com.tangem.ui.fragment.id.ValidateIdFragment"
android:label="ValidateIdFragment" >
<action
android:id="@+id/action_validateIdFragment_to_writeIdFragment"
app:destination="@id/writeIdFragment" />
</fragment>
<fragment
android:id="@+id/writeIdFragment"
android:name="com.tangem.ui.fragment.id.WriteIdFragment"
android:label="WriteIdFragment" />
</navigation>

View file

@ -9,9 +9,9 @@
<string name="general_continue">Continuer</string>
<string name="error_empty_pin">Le code PIN est vide</string>
<string name="general_blockchain">Blockchain</string>
<string name="general_notification_scan_again">Réessayez de numériser</string>
<string name="general_notification_scan_again">Réessayez de scanner la carte</string>
<string name="general_error_cannot_erase_wallet_with_non_zero_balance">Impossible d\'effacer le porte-monnaie avec un solde différent de zéro</string>
<string name="general_send_transaction">Envoyer le paiement</string>
<string name="general_send_transaction">Transférer les fonds</string>
<string name="general_from_card">Depuis la carte</string>
<string name="general_on_card">sur la carte</string>
<string name="general_balance">avec solde</string>
@ -49,9 +49,9 @@
<string name="dialog_the_nfc_adapter_length_apdu">Oups .. Il semble que votre smartphone ne supporte pas de tels paquets NFC.</string>
<string name="dialog_the_nfc_adapter_length_apdu_advice">Essayez d\'envoyer une quantité inférieure ou utilisez un smartphone avec prise en charge complète de NFC.</string>
<string name="dialog_title_money_is_at_risk">Votre argent est en jeu!</string>
<string name="dialog_this_card_has_enforced_security_delay">Cette carte a imposé un délai de sécurité</string>
<string name="dialog_hold_card">Veuillez tenir la carte fermement \ n jusqu\'à ce que l\'opération soit terminée…</string>
<string name="dialog_you_may_be_required_to_repeat">Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone. \ n Ceci est fait pour sécuriser vos fonds.</string>
<string name="dialog_this_card_has_enforced_security_delay">Cette carte impose un délai de sécurité pour confirmer le transfert des fonds</string>
<string name="dialog_hold_card">Veuillez maintenir la carte dans cette position jusqu\'à ce que l\'opération soit terminée…</string>
<string name="dialog_you_may_be_required_to_repeat">Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone.\nCeci est fait pour sécuriser vos fonds.</string>
<!-- menu main -->
<string name="main_menu_manage_pin_1">Gérer le code PIN de l\'utilisateur…</string>
@ -74,16 +74,18 @@
<!-- Main -->
<string name="main_screen_tap_card">Mettez en contact la carte avec votre smartphone</string>
<string name="main_screen_scan_card">Scanner une carte avec votre \n %1$s \n comme indiqué ci-dessus</string>
<string name="main_screen_scan_card">Scanner votre carte avec votre %1$s comme indiqué ci-dessus</string>
<string name="main_screen_phone">phone</string>
<string name="main_screen_erased_wallet">Le porte-monnaie a été effacé</string>
<string name="main_screen_not_personalized">Non personnalisé</string>
<string name="main_screen_new_version_toast">Il existe une nouvelle version de l\'application: %1$s</string>
<string name="main_screen_btn_update">Mettre à jour</string>
<string name="main_screen_visit_store">Vous navez pas de carte?\nVisitez notre boutique sur %1$s</string>
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">Aucun porte-monnaie compatible installé</string>
<string name="loaded_wallet_message_wait">Veuillez patienter pendant que la transaction précédente est confirmée dans la blockchain</string>
<string name="loaded_wallet_message_wait">Veuillez patienter que votre précédente transaction soit confirmée dans la blockchai</string>
<string name="loaded_wallet_message_refresh">Impossible d\'obtenir toutes les entrées. Balayez vers le bas pour actualiser.</string>
<string name="loaded_wallet_warning_no_signature">La carte n\'a plus de signature!</string>
<string name="loaded_wallet_warning_dont_forget_pin">Si vous oubliez votre nouveau code PIN, vous perdrez votre argent pour toujours!</string>
@ -130,7 +132,7 @@
<string name="balance_validator_second_line_confirmed_in_blockchain">Solde confirmé dans la blockchain</string>
<string name="balance_validator_second_line_unverified_balance">Solde ne peut pas être vérifié. Balayez vers le bas pour actualiser..</string>
<string name="balance_validator_second_line_wait_for_confirmation">Attendez la confirmation dans la chaîne de blocs</string>
<string name="balance_validator_second_line_wait_for_confirmation">En attente de confirmation dans la blockchain</string>
<string name="balance_validator_second_line_internet_to_verify_online">Restaurez la connexion Internet pour obtenir un solde sécurisé de la chaîne de blocs</string>
<string name="balance_validator_second_line_internet_to_get_balance">Impossible d\'obtenir le
solde de la blockchain. Restaurez la connexion Internet pour être plus confiant.</string>
@ -146,10 +148,10 @@
<string name="balance_validator_second_line_authenticity_not_verified">L\'authenticité ne peut pas être vérifiée. Balayez vers le bas pour actualiser.</string>
    
<!-- CreateNewWallet, Purge, SignTransaction -->
<string name="now_touch_the_card_with_id"> Maintenant, touchez la carte avec l\'ID </string>
<string name="now_touch_the_card_with_id">Maintenir la carte avec l\ID</string>
<string name="to_create_the_wallet">pour créer le porte-monnaie </string>
<string name="to_erase_the_wallet">pour effacer le porte-monnaie</string>
<string name="to_sign_the_transaction">pour signer le paiement</string>
<string name="to_sign_the_transaction">pour confirmer le transfert des fonds</string>
<string name="to_change_pin_codes">pour changer les codes PIN / PIN2</string>
<string name="nfc_purge_warning">En mettant la carte en contact avec votre smartphone, vous retirerez définitivement le porte-monnaie de la blockchain. Assurez-vous qu\'il n\'y aura pas d\'autres transactions entrantes</string>
<string name="nfc_error_cannot_erase_wallet">Impossible d\'effacer le porte-monnaie. Veillez à entrer le code PIN2 correct!</string>
@ -170,7 +172,7 @@
<!-- SendTransaction -->
<string name="send_transaction_notification_wait">Veuillez patienter pendant l\'envoi du paiement…</string>
<string name="send_transaction_success">La transaction a été signée et envoyée avec succès au noeud de la blockchain. La balance de porte-monnaie sera mise à jour dans un moment</string>
<string name="send_transaction_success">La transaction a été signée et envoyée avec succès au noeud de la blockchain. Le solde de votre porte-monnaie sera mise à jour dès que transaction sera validée sur la blockchain</string>
<string name="send_transaction_error_failed_to_send">Réessayez. Échec d\'envoi de la transaction (%s)</string>
<string name="send_transaction_error_cannot_sign">Impossible de signer la transaction. Veillez à entrer le code PIN2 correct!</string>
<string name="send_transaction_error_wrong_amount">Le montant et les frais sont supérieurs au solde total du porte-monnaie!</string>
@ -178,7 +180,7 @@
<!-- PrepareTransaction -->
<string name="prepare_transaction_hint_enter_address">entrez l\'adresse du porte-monnaie</string>
<string name="prepare_transaction_hint_enter_amount">entrer le montant</string>
<string name="prepare_transaction_btn_verify">Vérifiez</string>
<string name="prepare_transaction_btn_verify">Vérifier</string>
<string name="prepare_transaction_error_not_enough_funds">Pas assez de fonds</string>
<string name="prepare_transaction_error_unknown_amount_format">Format de montant inconnu</string>
<string name="prepare_transaction_error_incorrect_destination">Adresse du porte-monnaie de destination incorrecte</string>
@ -194,9 +196,9 @@
<string name="confirm_transaction_btn_fee_normal">Normal</string>
<string name="confirm_transaction_btn_fee_priority">Priorité</string>
<string name="confirm_transaction_btn_including_fee">Frais inclus</string>
<string name="confirm_transaction_btn_not_including_fee">Frais non compris</string>
<string name="confirm_transaction_btn_not_including_fee">Frais non inclus</string>
<string name="confirm_transaction_including_fee">(frais inclus)</string>
<string name="confirm_transaction_not_including_fee">(sans frais)</string>
<string name="confirm_transaction_not_including_fee">(frais non inclus)</string>
<string name="confirm_transaction_error_data_is_outdated">Les données obtenues sont obsolètes! Réessayez</string>
<string name="confirm_transaction_error_cannot_reach_node">Impossible d\'atteindre le nœud de la blockchain active en cours. Réessayez</string>
<string name="confirm_transaction_error_cannot_check_balance">Impossible de vérifier le solde! Pas de connexion avec les noeuds de la blockchain</string>
@ -223,12 +225,12 @@
<string name="details_category_manufacturer">Fabricant</string>
<string name="details_category_wallet">Porte-monnaie</string>
<string name="details_card_identity">Identité de la carte</string>
<string name="details_attested">Attested</string>
<string name="details_attested">Attestée</string>
<string name="details_not_confirmed">Non confirmé</string>
<string name="details_reusable">Réutilisable</string>
<string name="details_none">None</string>
<string name="details_last_one">Le dernier!</string>
<string name="details_unlimited">Unlimited</string>
<string name="details_unlimited">illimitées</string>
<string name="details_one_off_card">Carte ponctuelle</string>
<string name="details_firmware">Micrologiciel</string>
<string name="details_registration_date">Date d\'enregistrement</string>
@ -238,7 +240,7 @@
<string name="details_category_issuer">Émetteur</string>
<string name="details_private_key">Clé privée</string>
<string name="details_validation_node">Noeud de validation</string>
<string name="details_unspents">Unspents</string>
<string name="details_unspents">Transferts</string>
<string name="details_protected_by_default_pin_1">Cette carte est protégée par le code PIN1 par défaut</string>
<string name="details_protected_by_user_pin_1">Cette carte est protégée par le code PIN1 de lutilisateur</string>
<string name="details_protected_by_default_pin_2">Cette carte est protégée par le code PIN2 par défaut</string>
@ -255,8 +257,9 @@
<string name="details_blockable">Bloquable\n</string>
<string name="details_atomic_commmands">Mode de commande atomique\n</string>
<string name="details_linking_card_supported">La liaison au terminal est prise en charge\n</string>
<string name="details_linked_card_title">La carte est liée</string>
<string name="details_linked_card_title">La carte est associée</string>
<string name="details_linked_card_to_phone">à ce téléphone</string>
<string name="details_unspents_number">%1$d envoyé(s), %2$d reçu(s)</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Enregistrer</string>

View file

@ -82,7 +82,7 @@
<string name="main_screen_new_version_toast">There is a new application version: %1$s</string>
<string name="main_screen_btn_update">Update</string>
<string name="main_screen_visit_store">Don\'t have a card?\n Visit our store at %1$s</string>
<string name="main_screen_store_address">tangemcards.com</string>
<string name="main_screen_store_address" translatable="false">tangemcards.com</string>
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">No compatible wallets installed</string>
@ -262,6 +262,19 @@
<string name="details_linking_card_supported">Linking to the terminal is supported\n</string>
<string name="details_linked_card_title">The card is linked</string>
<string name="details_linked_card_to_phone">To this phone</string>
<string name="details_unspents_number">%1$d unspents (%2$d received)</string>
<!-- TagFragment -->
<string name="tag_claim">Claim</string>
<string name="tag_genuine">GENUINE</string>
<string name="tag_not_genuine">NOT GENUINE</string>
<string name="tag_claimed">ALREADY CLAIMED</string>
<!-- Id Card -->
<string name="id_verified">VERIFIED</string>
<string name="id_not_registered">NOT REGISTERED</string>
<string name="id_demo">ID DEMO</string>
<string name="id_empty">Empty ID card</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Save</string>

View file

@ -3,5 +3,6 @@
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
</resources>

View file

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.60'
ext.kotlin_version = '1.3.61'
repositories {
google()
jcenter()
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.android.tools.build:gradle:3.5.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath 'io.fabric.tools:gradle:1.31.0'

View file

@ -47,7 +47,7 @@ class LocalStorage
artworks = HashMap()
}
if (artworks.count() < 29) {
if (artworks.count() < 32) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
@ -70,6 +70,7 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru031, false)
putResourceArtworkToCatalog(R.drawable.card_ru032, false)
putResourceArtworkToCatalog(R.drawable.card_ru037, false)
putResourceArtworkToCatalog(R.drawable.card_ru038, false)
putResourceArtworkToCatalog(R.drawable.card_ru039, false)
putResourceArtworkToCatalog(R.drawable.card_ru040, false)
putResourceArtworkToCatalog(R.drawable.card_ru041, false)
@ -77,6 +78,8 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru043, false)
putResourceArtworkToCatalog(R.drawable.card_tg044, false)
putResourceArtworkToCatalog(R.drawable.card_tg046, false)
putResourceArtworkToCatalog(R.drawable.card_tgslix, false)
putResourceArtworkToCatalog(R.drawable.card_bc00, false)
putResourceArtworkToCatalog(R.drawable.card_ff32, true)
}
if (batchesFile.exists()) {
@ -237,6 +240,9 @@ class LocalStorage
hexCID in "CB25000000000000".."CB25000000099999" -> R.drawable.card_ru043
hexCID in "CB26000000000000".."CB26000000099999" -> R.drawable.card_tg044
//Sergio's business card
hexCID in "BC00000000000000".."BC99999999999999" -> R.drawable.card_bc00
card.batch == "0004" -> R.drawable.card_ru006
card.batch == "0006" -> R.drawable.card_ru006
card.batch == "0010" -> R.drawable.card_ru006

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -0,0 +1,12 @@
package com.tangem.tangem_card.data
enum class ProductMask(val code: Byte) {
Note(0x01),
Tag(0x02),
Id(0x04);
companion object {
private val values = values()
fun byCode(code: Byte): ProductMask? = values.find { it.code == code }
}
}

View file

@ -1,9 +1,14 @@
package com.tangem.tangem_card.data;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.reader.ProductMask;
import com.tangem.tangem_card.reader.SettingsMask;
import com.tangem.tangem_card.reader.TLV;
import com.tangem.tangem_card.reader.TLVList;
import com.tangem.tangem_card.util.Util;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
@ -29,6 +34,8 @@ public class TangemCard {
private byte[] terminalPrivateKey;
private byte[] terminalPublicKey;
private boolean terminalIsLinked = false;
private byte[] tagSignature = null;
private byte[] idHash = null;
public String getBlockchainID() {
return blockchainID;
@ -183,6 +190,14 @@ public class TangemCard {
this.terminalIsLinked = terminalIsLinked;
}
public byte[] getIdHash() {
return idHash;
}
public void setIdHash(byte[] idHash) {
this.idHash = idHash;
}
private int health = 0;
public int getHealth() {
@ -287,6 +302,28 @@ public class TangemCard {
return "";
}
private byte[] issuerDataEx;
private byte[] issuerDataExSignature;
private int issuerDataExCounter;
public byte[] getIssuerDataEx() {
return issuerDataEx;
}
public byte[] getIssuerDataExSignature() {
return issuerDataExSignature;
}
public int getIssuerDataExCounter() {
return issuerDataExCounter;
}
public void setIssuerDataEx(byte[] value, byte[] signature, int counter) {
issuerDataEx = value;
issuerDataExSignature = signature;
issuerDataExCounter = counter;
}
private int pauseBeforePIN2 = 0;
public void setPauseBeforePIN2(int value) {
@ -578,6 +615,101 @@ public class TangemCard {
this.UID = UID;
}
public int productMask = ProductMask.Note;
public void setProductMask(int productMask) {
this.productMask = productMask;
}
public boolean isIDCard() {
try {
return (productMask & ProductMask.IdCard) != 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public TLVList tlvIDCardData = null;
public void setTlvIdCardData(TLVList tlvIDCardData) {
this.tlvIDCardData = tlvIDCardData;
}
public void setTlvIDCardData(
String fullName,
String birthday,
String gender,
byte[] photo,
String issueDate,
String expireDate,
String trustedAddress) {
IDCardData data = new IDCardData();
data.fullName = fullName;
data.birthday = birthday;
data.gender = gender;
data.photo = photo;
data.issueDate = issueDate;
data.expireDate = expireDate;
data.trustedAddress = trustedAddress;
this.tlvIDCardData = data.toTLVList();
}
public boolean hasIDCardData() {
if (tlvIDCardData == null) {
if (issuerDataEx == null) return false;
try {
tlvIDCardData = TLVList.fromBytes(issuerDataEx);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
return tlvIDCardData.hasTag(TLV.Tag.TAG_FullName) && tlvIDCardData.hasTag(TLV.Tag.TAG_Birthday) && tlvIDCardData.hasTag(TLV.Tag.TAG_Gender) && tlvIDCardData.hasTag(TLV.Tag.TAG_Photo)
&& tlvIDCardData.hasTag(TLV.Tag.TAG_IssueDate) && tlvIDCardData.hasTag(TLV.Tag.TAG_ExpireDate) && tlvIDCardData.hasTag(TLV.Tag.TAG_TrustedAddress);
}
public static class IDCardData {
public String fullName;
public String birthday;
public String gender;
public byte[] photo;
public String issueDate;
public String expireDate;
public String trustedAddress;
public TLVList toTLVList() {
TLVList tlvIDCardData = new TLVList();
tlvIDCardData.add(new TLV(TLV.Tag.TAG_FullName, fullName.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_Birthday, birthday.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_Gender, gender.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_Photo, photo));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_IssueDate, issueDate.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_ExpireDate, expireDate.getBytes(StandardCharsets.UTF_8)));
tlvIDCardData.add(new TLV(TLV.Tag.TAG_TrustedAddress, trustedAddress.getBytes(StandardCharsets.UTF_8)));
return tlvIDCardData;
}
public static IDCardData fromTLVList(TLVList tlvIDCardData) {
IDCardData result = new IDCardData();
result.fullName = tlvIDCardData.getTLV(TLV.Tag.TAG_FullName).getAsString();
result.birthday = tlvIDCardData.getTLV(TLV.Tag.TAG_Birthday).getAsString();
result.gender = tlvIDCardData.getTLV(TLV.Tag.TAG_Gender).getAsString();
result.photo = tlvIDCardData.getTLV(TLV.Tag.TAG_Photo).Value;
result.issueDate = tlvIDCardData.getTLV(TLV.Tag.TAG_IssueDate).getAsString();
result.expireDate = tlvIDCardData.getTLV(TLV.Tag.TAG_ExpireDate).getAsString();
result.trustedAddress = tlvIDCardData.getTLV(TLV.Tag.TAG_TrustedAddress).getAsString();
return result;
}
}
public IDCardData getIDCardData() {
if (!hasIDCardData()) return null;
return IDCardData.fromTLVList(tlvIDCardData);
}
private byte[] offlineBalance;
@ -629,163 +761,13 @@ public class TangemCard {
DenominationText = null;
}
// public Bundle getAsBundle() {
// Bundle B = new Bundle();
// saveToBundle(B);
// return B;
// }
//
// public void saveToBundle(Bundle B) {
// try {
// B.putString("UID", UID);
// B.putByteArray("CID", CID);
// B.putString("PIN", PIN);
// B.putString("PIN2", PIN2.name());
// B.putString("Status", status.name());
// B.putString("Blockchain", blockchainID);
// B.putString("BlockchainName", blockchainName);
// B.putInt("TokensDecimal", tokensDecimal);
// B.putString("TokenSymbol", tokenSymbol);
// B.putString("ContractAddress", contractAddress);
// if (dtPersonalization != null) B.putLong("dtPersonalization", dtPersonalization.getTime());
// B.putInt("RemainingSignatures", remainingSignatures);
// B.putInt("MaxSignatures", maxSignatures);
// B.putInt("Health", health);
// if (settingsMask != null) B.putInt("settingsMask", settingsMask);
// B.putInt("pauseBeforePIN2", pauseBeforePIN2);
// if (signingMethod != null) B.putString("signingMethod", signingMethod.name());
// if (manufacturer != null) B.putString("Manufacturer", manufacturer.name());
// if (encryptionMode != null) B.putString("EncryptionMode", encryptionMode.name());
// if (issuer != null) B.putString("Issuer", issuer.getID());
// if (issuerPublicDataKey != null) B.putByteArray("IssuerPublicDataKey", issuerPublicDataKey);
// if (firmwareVersion != null) B.putString("FirmwareVersion", firmwareVersion);
// if (batch != null) B.putString("Batch", batch);
// B.putBoolean("ManufacturerConfirmed", manufacturerConfirmed);
// B.putBoolean("CardPublicKeyValid", isCardPublicKeyValid());
// B.putByteArray("CardPublicKey", getCardPublicKey());
//
// B.putInt("SignedHashes", getSignedHashes());
// B.putBoolean("WalletPublicKeyValid", isWalletPublicKeyValid());
// if (pbWalletKey != null)
// B.putByteArray("PublicKey", pbWalletKey);
// if (pbWalletKeyRar != null)
// B.putByteArray("PublicKeyRar", pbWalletKeyRar);
//
// if (getOfflineBalance() != null) B.putByteArray("OfflineBalance", getOfflineBalance());
//
// if (getDenomination() != null) B.putByteArray("Denomination", getDenomination());
// if (getDenominationText() != null) B.putString("DenominationText", getDenominationText());
//
// if (getIssuerData() != null && getIssuerDataSignature() != null) {
// B.putByteArray("IssuerData", getIssuerData());
// B.putByteArray("IssuerDataSignature", getIssuerDataSignature());
// B.putBoolean("NeedWriteIssuerData", getNeedWriteIssuerData());
// }
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (onlineVerified != null)
// B.putBoolean("onlineVerified", onlineVerified);
//
// if (onlineValidated != null)
// B.putBoolean("onlineValidated", onlineValidated);
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (codeConfirmed != null)
// B.putBoolean("codeConfirmed", codeConfirmed);
//
// if (onlineVerified != null)
// B.putBoolean("onlineVerified", onlineVerified);
//
// if (onlineValidated != null)
// B.putBoolean("onlineValidated", onlineValidated);
// } catch (Exception e) {
// Log.e("Can't save to bundle ", e.getMessage());
// }
//
// }
//
// public void loadFromBundle(Bundle B) {
// UID = B.getString("UID");
// CID = B.getByteArray("CID");
// PIN = B.getString("PIN");
// PIN2 = PIN2_Mode.valueOf(B.getString("PIN2"));
// status = Status.valueOf(B.getString("Status"));
// blockchainID = B.getString("Blockchain");
// tokensDecimal = B.getInt("TokensDecimal", 18);
// tokenSymbol = B.getString("TokenSymbol", "");
// contractAddress = B.getString("ContractAddress", "");
// if (B.containsKey("BlockchainName"))
// blockchainName = B.getString("BlockchainName", "");
// if (B.containsKey("dtPersonalization")) {
// dtPersonalization = new Date(B.getLong("dtPersonalization"));
// }
// remainingSignatures = B.getInt("RemainingSignatures");
// maxSignatures = B.getInt("MaxSignatures");
// health = B.getInt("health");
// if (B.containsKey("settingsMask")) settingsMask = B.getInt("settingsMask");
// pauseBeforePIN2 = B.getInt("pauseBeforePIN2");
// if (B.containsKey("signingMethod"))
// signingMethod = SigningMethod.valueOf(B.getString("signingMethod"));
// if (B.containsKey("Manufacturer"))
// manufacturer = Manufacturer.valueOf(B.getString("Manufacturer"));
// manufacturerConfirmed = B.getBoolean("ManufacturerConfirmed");
// if (B.containsKey("EncryptionMode"))
// encryptionMode = EncryptionMode.valueOf(B.getString("EncryptionMode"));
// else
// encryptionMode = null;
//
// if (B.containsKey("SignedHashes")) setSignedHashes(B.getInt("SignedHashes"));
//
// if (B.containsKey("Issuer")) issuer = Issuer.FindIssuer(B.getString("Issuer"));
// if (B.containsKey("IssuerPublicDataKey"))
// issuerPublicDataKey = B.getByteArray("IssuerPublicDataKey");
//
// if (B.containsKey("FirmwareVersion")) firmwareVersion = B.getString("FirmwareVersion");
// if (B.containsKey("Batch")) batch = B.getString("Batch");
//
// cardPublicKeyValid = B.getBoolean("CardPublicKeyValid");
// if (B.containsKey("CardPublicKey")) setCardPublicKey(B.getByteArray("CardPublicKey"));
//
// if (B.containsKey("OfflineBalance")) setOfflineBalance(B.getByteArray("OfflineBalance"));
// else clearOfflineBalance();
//
// if (B.containsKey("Denomination") && B.containsKey("DenominationText")) {
// setDenomination(B.getByteArray("Denomination"), B.getString("DenominationText"));
// } else if (B.containsKey("Denomination")) {
// setDenomination(B.getByteArray("Denomination"), "N/A");
// } else clearDenomination();
//
// if (B.containsKey("IssuerData") && B.containsKey("IssuerDataSignature"))
// setIssuerData(B.getByteArray("IssuerData"), B.getByteArray("IssuerDataSignature"));
// else setIssuerData(null, null);
//
// if (B.containsKey("NeedWriteIssuerData"))
// setNeedWriteIssuerData(B.getBoolean("NeedWriteIssuerData"));
//
// walletPublicKeyValid = B.getBoolean("WalletPublicKeyValid");
// if (B.containsKey("PublicKey")) {
// pbWalletKey = B.getByteArray("PublicKey");
// }
// if (B.containsKey("PublicKeyRar")) {
// pbWalletKeyRar = B.getByteArray("PublicKeyRar");
// }
//
// if (B.containsKey("codeConfirmed"))
// codeConfirmed = B.getBoolean("codeConfirmed");
//
// if (B.containsKey("onlineVerified"))
// onlineVerified = B.getBoolean("onlineVerified");
//
// if (B.containsKey("onlineValidated"))
// onlineValidated = B.getBoolean("onlineValidated");
// }
public byte[] getTagSignature() {
return tagSignature;
}
public void setTagSignature(byte[] tagSignature) {
this.tagSignature = tagSignature;
}
public enum EncryptionMode {
None((byte) 0x0), Fast((byte) 0x1), Strong((byte) 0x2);

View file

@ -1,5 +1,6 @@
package com.tangem.tangem_card.reader;
import com.tangem.tangem_card.data.Issuer;
import com.tangem.tangem_card.data.Manufacturer;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.util.Log;
@ -17,6 +18,7 @@ import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import javax.crypto.KeyAgreement;
@ -99,6 +101,11 @@ public class CardProtocol {
private NfcReader mIsoDep;
public NfcReader getReader()
{
return mIsoDep;
}
public static final String DefaultPIN = "000000";
public static final String DefaultPIN2 = "000";
@ -957,9 +964,11 @@ public class CardProtocol {
}
private void prepareDataForLinkingTerminal(CommandApdu apdu, byte[] data) throws Exception {
byte[] transactionSignature = CardCrypto.Signature(mCard.getTerminalPrivateKey(), data);
apdu.addTLV(TLV.Tag.TAG_Terminal_TransactionSignature, transactionSignature);
addTerminalPublicKeyToApdu(apdu);
if (mCard.getTerminalPrivateKey() != null) {
byte[] transactionSignature = CardCrypto.Signature(mCard.getTerminalPrivateKey(), data);
apdu.addTLV(TLV.Tag.TAG_Terminal_TransactionSignature, transactionSignature);
addTerminalPublicKeyToApdu(apdu);
}
}
/**
@ -1095,6 +1104,12 @@ public class CardProtocol {
if (issuerData == null || issuerDataSignature == null)
throw new TangemException("Invalid answer format (GetIssuerData)");
if( issuerData.Value.length==0 && issuerDataSignature.Value.length==0 )
{
mCard.setIssuerData(null, null);
return new TLVList();
}
ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream();
bsDataToVerify.write(mCard.getCID());
bsDataToVerify.write(issuerData.Value);
@ -1117,6 +1132,185 @@ public class CardProtocol {
}
}
/**
* Series of GET_ISSUER_DATA command to read extra data and verify issuer signature of returned extra data
* See [1] 3.3, 8.7
* This command returns Issuer_Extra_Data data block and its issuers signature.
*
* @return TLVList with issuerData (if success read and verify)
* @throws Exception - if something went wrong
*/
public TLVList run_GetIssuerDataEx(Notifications notifications) throws Exception {
boolean needReadIssuerDataCounter = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
TLV issuerDataSignature = null;
TLV issuerDataCounter = null;
TLV issuerDataSize = null;
ByteArrayOutputStream bsIssuerDataEx = new ByteArrayOutputStream();
do {
CommandApdu rqApdu = StartPrepareCommand(INS.GetIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x01);
rqApdu.addTLV_U16(TLV.Tag.TAG_Offset, bsIssuerDataEx.size());
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
ResponseApdu rspApdu = SendAndReceive(rqApdu, false);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
TLV issuerData = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data);
if (issuerData != null && issuerData.Value != null)
bsIssuerDataEx.write(issuerData.Value);
issuerDataSignature = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Signature);
issuerDataCounter = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Issuer_Data_Counter);
if (issuerDataSize == null)
issuerDataSize = rspApdu.getTLVs().getTLV(TLV.Tag.TAG_Size);
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
if (issuerDataSize != null && issuerDataSize.getAsInt() != 0) {
if (notifications != null)
notifications.onReadProgress(this, (int) Math.round(bsIssuerDataEx.size() * 100.0 / issuerDataSize.getAsInt()));
Log.i(logTag, String.format("Read %d/%d bytes", bsIssuerDataEx.size(), issuerDataSize.getAsInt()));
}
} while ((issuerDataSignature == null) || (needReadIssuerDataCounter && (issuerDataCounter == null)));
boolean protectIssuerDataAgainstReplay = (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
if (notifications != null) notifications.onReadProgress(this, 100);
if (bsIssuerDataEx.size() > 0 && issuerDataSignature != null && issuerDataSignature.Value != null) {
ByteArrayOutputStream bsDataToVerify = new ByteArrayOutputStream();
bsDataToVerify.write(mCard.getCID());
bsDataToVerify.write(bsIssuerDataEx.toByteArray());
if (protectIssuerDataAgainstReplay) {
bsDataToVerify.write(issuerDataCounter.Value);
}
try {
if (CardCrypto.VerifySignature(mCard.getIssuerPublicDataKey(), bsDataToVerify.toByteArray(), issuerDataSignature.Value)) {
mCard.setIssuerDataEx(bsIssuerDataEx.toByteArray(), issuerDataSignature.Value, issuerDataCounter.getAsInt());
return TLVList.fromBytes(bsIssuerDataEx.toByteArray());
} else {
throw new TangemException("Invalid issuer data read (signature verification failed)");
}
} catch (Exception e) {
e.printStackTrace();
throw new TangemException("Invalid issuer data read");
}
} else {
mCard.setIssuerDataEx(null, null, issuerDataCounter.getAsInt());
return null;
}
}
/**
* Series of WRITE_ISSUER_DATA command to write extra data:
* 1. start write procedure
* 2. series of writing data blocks
* 3. finalize write procedure
* This command make signatures with provided issuer privateDataKey
* - to start write procedure (sign CID|Counter|Size)
* - to finalize write procedure (sign CID|dataEx|Counter)
* See [1] 3.3, 8.8
*
* @throws Exception - if something went wrong
*/
public void run_WriteIssuerDataEx(Notifications notifications, byte[] dataEx, byte[] issuerPrivateDataKey) throws Exception {
boolean needCounter = readResult.getTLV(TLV.Tag.TAG_SettingsMask) != null && (readResult.getTagAsInt(TLV.Tag.TAG_SettingsMask) & SettingsMask.ProtectIssuerDataAgainstReplay) != 0;
Log.i(logTag, "WriteIssuerDataEx - Start");
int issuerDataCounter = 0;
ByteArrayOutputStream osDataToSign = new ByteArrayOutputStream();
osDataToSign.write(mCard.getCID());
if (needCounter) {
issuerDataCounter = mCard.getIssuerDataExCounter() + 1;
osDataToSign.write(Util.intToByteArray4(issuerDataCounter));
}
osDataToSign.write(Util.intToByteArray2(dataEx.length));
byte[] issuerDataSignature = CardCrypto.Signature(issuerPrivateDataKey, osDataToSign.toByteArray());
CommandApdu rqApdu = StartPrepareCommand(INS.WriteIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x01);
if (needCounter) {
rqApdu.addTLV_U32(TLV.Tag.TAG_Issuer_Data_Counter, issuerDataCounter);
}
rqApdu.addTLV_U16(TLV.Tag.TAG_Size, dataEx.length);
rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature);
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
ResponseApdu rspApdu = SendAndReceive(rqApdu, true);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
if (notifications != null)
notifications.onReadProgress(this, 10);
Log.i(logTag, "WriteIssuerDataEx - Send data");
int offset = 0;
int partSize = 1524;
while (offset < dataEx.length) {
if (dataEx.length - offset < partSize) {
partSize = dataEx.length - offset;
}
byte[] part = Arrays.copyOfRange(dataEx, offset, offset + partSize);
rqApdu = StartPrepareCommand(INS.WriteIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x02);
rqApdu.addTLV_U16(TLV.Tag.TAG_Offset, offset);
rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data, part);
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
rspApdu = SendAndReceive(rqApdu, true);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
offset += partSize;
if (notifications != null)
notifications.onReadProgress(this, 10+(int) Math.round(offset * 80.0 / dataEx.length));
}
Log.i(logTag, "WriteIssuerDataEx - Finalize");
osDataToSign = new ByteArrayOutputStream();
osDataToSign.write(mCard.getCID());
osDataToSign.write(dataEx);
if (needCounter) {
osDataToSign.write(Util.intToByteArray4(issuerDataCounter));
}
issuerDataSignature = CardCrypto.Signature(issuerPrivateDataKey, osDataToSign.toByteArray());
rqApdu = StartPrepareCommand(INS.WriteIssuerData);
rqApdu.addTLV_U8(TLV.Tag.TAG_Mode, 0x03);
rqApdu.addTLV(TLV.Tag.TAG_Issuer_Data_Signature, issuerDataSignature);
Log.i(logTag, String.format("[%s]\n%s", rqApdu.getCommandName(), rqApdu.getTLVs().getParsedTLVs(" ")));
rspApdu = SendAndReceive(rqApdu, true);
if (rspApdu.isStatus(SW.PROCESS_COMPLETED)) {
Log.i(logTag, String.format("OK: [%04X]\n%s", rspApdu.getSW1SW2(), rspApdu.getTLVs().getParsedTLVs(" ")));
} else {
throw new TangemException(String.format("Failed: %04X", rspApdu.getSW1SW2()));
}
if (notifications != null)
notifications.onReadProgress(this, 100);
}
/**
* Execute consecutive READ commands with increasing encryption level from EncryptionMode.None to EncryptionMode.Strong, see {@link TangemCard.EncryptionMode}
* If card requires stricter encryption level it returns SW.NEED_ENCRYPTION status word

View file

@ -17,4 +17,5 @@ public interface NfcReader {
void connect();
boolean isConnected();
}

View file

@ -0,0 +1,25 @@
package com.tangem.tangem_card.reader;
/**
* Created by dvol on 15.07.2019.
*/
public class ProductMask {
public static final int Note = 0x0001;
public static final int Tag = 0x0002;
public static final int IdCard = 0x0004;
public static String getDescription(int iValue) {
StringBuilder sb = new StringBuilder();
sb.append("[");
if ((iValue & ProductMask.Note) != 0) sb.append("Note, ");
if ((iValue & ProductMask.Tag) != 0)
sb.append("Tag, ");
if ((iValue & ProductMask.IdCard) != 0)
sb.append("IdCard, ");
if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length());
sb.append("]");
return sb.toString();
}
}

View file

@ -29,8 +29,16 @@ public class SettingsMask {
public static final int DisablePrecomputedNDEF = 0x00010000;
public static final int SkipSecurityDelayIfValidatedByIssuer = 0x00020000;
public static final int SkipCheckPIN2andCVCIfValidatedByIssuer = 0x00040000;
public static final int SkipSecurityDelayIfValidatedByLinkedTerminal = 0x00080000;
public static final int RestrictOverwriteIssuerDataEx = 0x00100000;
public static final int RequireTermTxSignature = 0x01000000;
public static final int RequireTermCertSignature = 0x02000000;
public static final int CheckPIN3onCard = 0x04000000;
public static String getDescription(int iValue) {
StringBuilder sb=new StringBuilder();
sb.append("[");
@ -63,6 +71,24 @@ public class SettingsMask {
if ((iValue & SettingsMask.SkipSecurityDelayIfValidatedByLinkedTerminal) != 0)
sb.append("SkipSecurityDelayIfValidatedByLinkedTerminal, ");
if ((iValue & SettingsMask.SkipSecurityDelayIfValidatedByIssuer) != 0)
sb.append("SkipSecurityDelayIfValidatedByIssuer, ");
if ((iValue & SettingsMask.SkipCheckPIN2andCVCIfValidatedByIssuer) != 0)
sb.append("SkipCheckPIN2andCVCIfValidatedByIssuer, ");
if ((iValue & SettingsMask.RestrictOverwriteIssuerDataEx) != 0)
sb.append("RestrictOverwriteIssuerDataEx, ");
if ((iValue & SettingsMask.RequireTermTxSignature) != 0)
sb.append("RequireTermTxSignature, ");
if ((iValue & SettingsMask.RequireTermCertSignature) != 0)
sb.append("RequireTermCertSignature, ");
if ((iValue & SettingsMask.CheckPIN3onCard) != 0)
sb.append("CheckPIN3onCard, ");
if (sb.length() > 1) sb.delete(sb.length() - 2, sb.length());
sb.append("]");
return sb.toString();

View file

@ -44,15 +44,27 @@ public class TLV {
TAG_Session_Key_B(0x1B),
TAG_Pause(0x1C),
TAG_PIN3(0x1D),
TAG_NewPIN3(0x1E),
TAG_CrEx_Key(0x1F),
TAG_Manufacture_ID(0x20),
TAG_Manufacturer_Signature(0x21),
TAG_Mode(0x23),
TAG_Offset(0x24),
TAG_Size(0x25),
TAG_User_Data(0x2A),
TAG_User_ProtectedData(0x2B),
TAG_Issuer_Data_PublicKey(0x30),
TAG_Issuer_Transaction_PublicKey(0x31),
TAG_Issuer_Data(0x32),
TAG_Issuer_Data_Signature(0x33),
TAG_Issuer_Transaction_Signature(0x34),
TAG_Issuer_Data_Counter(0x35),
TAG_Acquirer_PublicKey(0x37),
TAG_IsActivated(0x3A),
TAG_ActivationSeed(0x3B),
@ -65,12 +77,38 @@ public class TLV {
TAG_TrOut_Hash(0x50),
TAG_TrOut_HashSize(0x51),
TAG_TrOut_Raw(0x52),
TAG_TrOut_Amount(0x53),
TAG_Terminal_PaymentFlowVersion(0x54),
TAG_Terminal_Certificate(0x55),
// hash of extra data to transaction (for SIGN_HASH_EXTERNAL)
TAG_TrOut_Extra_Hash(0x56),
TAG_Terminal_Transaction_Signature(0x57),
TAG_Terminal_Certificate_Id(0x5A),
TAG_Terminal_Certificate_Param(0x5B),
TAG_Terminal_Certificate_PublicKey(0x5C),
TAG_Terminal_Certificate_Signature(0x5D),
TAG_Wallet_PublicKey(0x60),
TAG_Signature(0x61),
TAG_RemainingSignatures(0x62),
TAG_SignedHashes(0x63),
TAG_CheckWalletCounter(0x64),
TAG_CrEx_Salt(0x65),
TAG_CrEx_Message(0x66),
TAG_CrEx_CryptedMessage(0x67),
TAG_CrEx_HMAC(0x68),
TAG_CrEx_RequirePIN3(0x69),
TAG_TrOut_Extra_Signature(0x6A),
TAG_SignCommand_ChecksPassed(0x6B),
TAG_Wallet_PrivateKey(0x70),
TAG_Card_PrivateKey(0x71),
TAG_Block_Reason(0x72),
TAG_Firmware(0x80),
TAG_Batch(0x81),
TAG_ManufactureDateTime(0x82),
@ -79,6 +117,10 @@ public class TLV {
TAG_Manufacturer_PublicKey(0x85),
TAG_CardID_Manufacturer_Signature(0x86),
TAG_ProductMask(0x8A),
TAG_PinLessFloorLimit(0x90),
TAG_Token_Symbol(0xA0),
TAG_Token_Contract_Address(0xA1),
TAG_Token_Decimal(0xA2),
@ -87,6 +129,14 @@ public class TLV {
TAG_LastSign_Date(0xC2),
TAG_DenominationText(0xC3),
TAG_FullName (0xD0),
TAG_Birthday (0xD1),
TAG_Photo (0xD2),
TAG_Gender(0xD3),
TAG_IssueDate (0xD4),
TAG_ExpireDate(0xD5),
TAG_TrustedAddress (0xD6),
TAG_Terminal_IsLinked(0x58),
TAG_Terminal_PublicKey(0x5C),
TAG_Terminal_TransactionSignature(0x57);
@ -172,7 +222,7 @@ public class TLV {
}
public String getAsString() {
if( Value.length==0 ) return "";
if (Value.length == 0) return "";
if (Value[Value.length - 1] == 0) {
String s1 = new String(Arrays.copyOfRange(Value, 0, Value.length - 1), Charset.forName("utf-8"));
@ -214,18 +264,29 @@ public class TLV {
return String.format("%s[]: [[NULL]]", tag.name());
}
case TAG_SettingsMask: {
StringBuilder sb=new StringBuilder();
if( Value!=null ) {
StringBuilder sb = new StringBuilder();
if (Value != null) {
try {
int iValue = Util.byteArrayToInt(Value);
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), SettingsMask.getDescription(iValue));
}
catch (Exception e)
{
} catch (Exception e) {
e.printStackTrace();
return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value));
}
}else{
} else {
return String.format("%s[]: [[NULL]]", tag.name());
}
}
case TAG_ProductMask: {
if (Value != null) {
try {
int iValue = Util.byteArrayToInt(Value);
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), ProductMask.getDescription(iValue));
} catch (Exception e) {
e.printStackTrace();
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value));
}
} else {
return String.format("%s[]: [[NULL]]", tag.name());
}
}

View file

@ -9,10 +9,11 @@ import com.tangem.tangem_card.util.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
public class TLVList extends ArrayList<TLV> {
public class TLVList extends ArrayList<TLV> implements Serializable {
public String getParsedTLVs(String Prefix) {
String parsed = "";
for (int i = 0; i < size(); i++) {
@ -69,4 +70,24 @@ public class TLVList extends ArrayList<TLV> {
while (tlv != null);
return tlvList;
}
public static TLVList tryFromBytes(byte[] mData) {
TLVList tlvList = new TLVList();
ByteArrayInputStream stream = new ByteArrayInputStream(mData);
TLV tlv = null;
do {
try {
tlv = TLV.ReadFromStream(stream);
if (tlv != null) tlvList.add(tlv);
} catch (IOException e) {
break;
}
}
while (tlv != null);
return tlvList;
}
public boolean hasTag(TLV.Tag tag) {
return getTLV(tag)!=null;
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.tangem_card.data.external.PINsProvider;
import com.tangem.tangem_card.reader.CardCrypto;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.reader.NfcReader;
import com.tangem.tangem_card.reader.ProductMask;
import com.tangem.tangem_card.reader.TLV;
import com.tangem.tangem_card.reader.TLVException;
import com.tangem.tangem_card.reader.TLVList;
@ -137,6 +138,12 @@ public class CustomReadCardTask extends Thread {
mCard.setBatch(tlvCardData.getTLV(TLV.Tag.TAG_Batch).getAsHexString());
if( tlvCardData.hasTag(TLV.Tag.TAG_ProductMask)) {
mCard.setProductMask(tlvCardData.getTagAsInt(TLV.Tag.TAG_ProductMask));
}else{
mCard.setProductMask(ProductMask.Note);
}
TLV tokenSymbol = tlvCardData.getTLV(TLV.Tag.TAG_Token_Symbol);
TLV contractAddress = tlvCardData.getTLV(TLV.Tag.TAG_Token_Contract_Address);
TLV tokens_decimal = tlvCardData.getTLV(TLV.Tag.TAG_Token_Decimal);

View file

@ -46,6 +46,10 @@ public class VerifyCardTask extends CustomReadCardTask {
}
mNotifications.onReadProgress(protocol, 90);
if( protocol.getCard().isIDCard() )
{
protocol.run_GetIssuerDataEx(mNotifications);
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tangem_card.tasks;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.data.external.CardDataSubstitutionProvider;
import com.tangem.tangem_card.data.external.PINsProvider;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.reader.NfcReader;
/**
* Created by dvol on 19.01.2020.
*/
public class WriteIDCardTask extends CustomReadCardTask {
public static final String TAG = WriteIDCardTask.class.getSimpleName();
private TangemCard.IDCardData idCardData;
private byte[] issuerPrivateDataKey;
public WriteIDCardTask(TangemCard card, NfcReader reader, CardDataSubstitutionProvider cardDataSubstitutionProvider, PINsProvider pinsProvider, TangemCard.IDCardData idCardData, byte[] issuerPrivateDataKey, CardProtocol.Notifications notifications) {
super(card, reader, cardDataSubstitutionProvider, pinsProvider, notifications);
this.idCardData=idCardData;
this.issuerPrivateDataKey=issuerPrivateDataKey;
}
@Override
public void run_Task() throws Exception {
mNotifications.onReadProgress(protocol, 0);
if (isCancelled) return;
if( !protocol.getCard().isIDCard() )
throw new Exception("It's not an IDCard!");
protocol.run_WriteIssuerDataEx(mNotifications,idCardData.toTLVList().toBytes(),issuerPrivateDataKey);
mNotifications.onReadProgress(protocol, 100);
}
}

View file

@ -10,7 +10,8 @@ data class CardEnvironment(
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionKey: ByteArray? = null
val encryptionKey: ByteArray? = null,
val cvc: ByteArray? = null
) {
companion object {

View file

@ -122,6 +122,37 @@ class CardManager(
runTask(task, cardId, callback)
}
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
* @param cardId CID, Unique Tangem card ID number.
*/
fun createWallet(cardId: String,
callback: (result: TaskEvent<CreateWalletResponse>) -> Unit) {
val createWalletCommand = CreateWalletCommand(cardId)
val task = SingleCommandTask(createWalletCommand)
runTask(task, cardId, callback)
}
/**
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
* If Is_Reusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
* @param cardId CID, Unique Tangem card ID number.
*/
fun purgeWallet(cardId: String,
callback: (result: TaskEvent<PurgeWalletResponse>) -> Unit) {
val purgeWalletCommand = PurgeWalletCommand(cardId)
val task = SingleCommandTask(purgeWalletCommand)
runTask(task, cardId, callback)
}
/**
*/
@ -157,6 +188,6 @@ class CardManager(
}
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
return cardEnvironmentRepository[cardId] ?: CardEnvironment()
return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId)
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @property cardId CID, Unique Tangem card ID number.
*/
class CreateWalletCommand(
private val cardId: String
) : CommandSerializer<CreateWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
if (cardEnvironment.cvc != null) {
tlvData.add(Tlv(TlvTag.Cvc, cardEnvironment.cvc))
}
return CommandApdu(Instruction.CreateWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
CreateWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status),
walletPublicKey = mapper.map(TlvTag.WalletPublicKey)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class PurgeWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus
) : CommandResponse
/**
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
* If Is_Reusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
* @property cardId CID, Unique Tangem card ID number.
*/
class PurgeWalletCommand(
private val cardId: String
) : CommandSerializer<PurgeWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
return CommandApdu(Instruction.PurgeWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
PurgeWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status))
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) {
Pause(0x1C),
ManufactureId(0x20),
ManufacturerSignature(0x21),
ManufacturerSignature(0x86),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),

View file

@ -45,6 +45,8 @@ class MainActivity : AppCompatActivity() {
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
}
}
@ -103,6 +105,32 @@ class MainActivity : AppCompatActivity() {
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
cardManager.purgeWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
btn_create_wallet?.setOnClickListener { _ ->
cardManager.createWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
}
private fun createSampleHashes(): Array<ByteArray> {

View file

@ -9,12 +9,12 @@
<TextView
android:paddingBottom="48dp"
android:layout_marginTop="48dp"
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_marginBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -25,6 +25,7 @@
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read and verify card"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -63,5 +64,27 @@
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_create_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Create wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_purge_wallet"
android:enabled="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -24,7 +24,8 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb
val TAG: String = NfcManager::class.java.simpleName
// reader mode flags: listen for type A (not B), skipping ndef check
private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_V or
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
private const val DELAY_PRESENCE = 1500
private const val REQUEST_NFC_PERMISSIONS = 1

View file

@ -38,4 +38,8 @@ data class NfcReader(
isoDep.timeout = timeout
}
override fun isConnected(): Boolean {
return isoDep.isConnected
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tangem_sdk.android.reader
import android.nfc.tech.NfcV
data class NfcVReader(
var nfcV: NfcV?
) : com.tangem.tangem_card.reader.NfcReader {
override fun getId(): ByteArray? {
return nfcV?.tag?.id
}
override fun setTimeout(timeout: Int) {
}
override fun getTimeout(): Int {
return 0
}
override fun transceive(data: ByteArray?): ByteArray? {
return nfcV?.transceive(data)
}
override fun ignoreTag() {
nfcV?.close()
nfcV = null
}
override fun notifyReadResult(success: Boolean) {
}
override fun connect() {
nfcV?.connect()
}
override fun isConnected(): Boolean {
return nfcV?.isConnected == true
}
}

View file

@ -0,0 +1,120 @@
package com.tangem.tangem_sdk.android.reader
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import com.tangem.tangem_card.reader.NfcReader
import com.tangem.tangem_card.reader.TLV
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.util.Util
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
class ReadSlixTagTask(val nfcReader: NfcReader) {
private var lastErrorCode: Int = 0
fun read(): ReadResult {
if (!nfcReader.isConnected) {
try {
nfcReader.connect()
} catch (e: Exception) {
return ReadResult.Failure(e)
}
}
return try {
runRead()
} catch (e: Exception) {
nfcReader.ignoreTag()
ReadResult.Failure(e)
}
}
private fun runRead(): ReadResult {
val ndefMessage = runReadNDEF()
val records: Array<NdefRecord> = ndefMessage.records
for (record in records) {
if (record.toUri() != null && record.toUri().toString() == "vnd.android.nfc://ext/tangem.com:wallet") {
val payload = record.payload
val tlvNDEF = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size))
nfcReader.ignoreTag()
return ReadResult.Success(tlvNDEF)
}
}
return ReadResult.Failure(Exception("No parcelable Tlv has been found."))
}
private fun runReadNDEF(): NdefMessage {
val answerCC = readSingleBlock(0x00)
if (answerCC.size != 4 || answerCC[0].toInt() != 0xE1 || ((answerCC[1].toInt() and 0xF0) != 0x40)) {
} else {
throw Exception("Failed! Invalid CC read " + Util.bytesToHex(answerCC))
}
if ((answerCC[3].toInt() and 0x01) != 0x01) {
throw Exception("Multiple block read unsupported!")
}
val areaSize = 8 * answerCC[2]
val blocksCount = areaSize / 4
val areaBuf = readMultipleBlocks(1, blocksCount)
val tlvNDEF = TLVList.tryFromBytes(areaBuf)
return NdefMessage(tlvNDEF.getTLV(TLV.Tag.TAG_CardPublicKey).Value)
}
private fun readSingleBlock(blockNo: Int): ByteArray {
return doTransceive(0x20, blockNo)!!
}
private fun readMultipleBlocks(startBlock: Int, blocksCount: Int): ByteArray {
val resultBuf = ByteArrayOutputStream()
val maxBlocksAtOnce = 32
var blocksRemaining = blocksCount
var firstBlockToRead = startBlock
while (blocksRemaining > 0) {
val blocksToRead = if (blocksRemaining > maxBlocksAtOnce) {
maxBlocksAtOnce
} else {
blocksRemaining
}
val blocks = doTransceive(0x23.toByte(), firstBlockToRead, blocksToRead - 1)
blocksRemaining -= blocksToRead
firstBlockToRead += blocksToRead
resultBuf.write(blocks!!)
}
return resultBuf.toByteArray()
}
private fun doTransceive(cmd: Byte, p1: Int?, p2: Int? = null, params: ByteArray? = null): ByteArray? {
val command: ByteArray
val res: ByteArray?
lastErrorCode = -1
val os = ByteArrayOutputStream()
os.write(REQ_FLAG.toInt())
os.write(cmd.toInt())
p1?.let { os.write(p1) }
p2?.let { os.write(it) }
params?.let { os.write(params, 0, params.size) }
command = os.toByteArray()
if (!nfcReader.isConnected) throw IOException("Connection lost")
res = nfcReader.transceive(command)
lastErrorCode = res[0].toInt()
if (lastErrorCode != 0) {
throw IOException("Error! Code: " + String.format("0x%02x", lastErrorCode))
}
return res.copyOfRange(1, res.size)
}
companion object{
// iso15693 flags
private const val REQ_FLAG: Byte = 0x02
}
}
sealed class ReadResult {
data class Success(val tlvs: TLVList) : ReadResult()
data class Failure(val exception: Exception) : ReadResult()
}

View file

@ -3,6 +3,7 @@ package com.tangem.tangem_sdk.data
import android.os.Bundle
import com.tangem.tangem_card.data.Manufacturer
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.util.Log
import java.util.*
@ -61,6 +62,13 @@ fun TangemCard.loadFromBundle(B: Bundle) {
setIssuerData(B.getByteArray("IssuerData"), B.getByteArray("IssuerDataSignature"))
else setIssuerData(null, null)
if (B.containsKey("IssuerDataEx") && B.containsKey("IssuerDataExSignature") && B.containsKey("IssuerDataExCounter"))
setIssuerDataEx(B.getByteArray("IssuerDataEx"), B.getByteArray("IssuerDataExSignature"), B.getInt("IssuerDataExCounter"))
else setIssuerDataEx(null, null, 0)
if( B.containsKey("ProductMask") )
setProductMask(B.getInt("ProductMask"))
if (B.containsKey("NeedWriteIssuerData"))
needWriteIssuerData = B.getBoolean("NeedWriteIssuerData")
@ -85,6 +93,10 @@ fun TangemCard.loadFromBundle(B: Bundle) {
if (B.containsKey("terminalPublicKey")) terminalPublicKey = B.getByteArray("terminalPublicKey")
terminalIsLinked = B.getBoolean("terminalIsLinked")
if (B.containsKey("tagSignature")) tagSignature = B.getByteArray("tagSignature")
if (B.containsKey("idHash")) idHash = B.getByteArray("idHash")
if (B.containsKey("tlvIDCardData")) tlvIDCardData = B.getSerializable("tlvIDCardData") as TLVList
}
val TangemCard.asBundle: Bundle
@ -112,11 +124,10 @@ fun TangemCard.saveToBundle(B: Bundle) {
B.putInt("Health", health)
if (settingsMask != null) B.putInt("settingsMask", settingsMask)
B.putInt("pauseBeforePIN2", pauseBeforePIN2)
if( allowedSigningMethod!=null ){
var iSigningMethod=0x80
for(sM in allowedSigningMethod)
{
iSigningMethod=iSigningMethod.or(0x01.shl(sM.ID))
if (allowedSigningMethod != null) {
var iSigningMethod = 0x80
for (sM in allowedSigningMethod) {
iSigningMethod = iSigningMethod.or(0x01.shl(sM.ID))
}
B.putInt("signingMethod", iSigningMethod)
}
@ -148,6 +159,14 @@ fun TangemCard.saveToBundle(B: Bundle) {
B.putBoolean("NeedWriteIssuerData", needWriteIssuerData)
}
if (issuerDataEx != null && issuerDataExSignature != null) {
B.putByteArray("IssuerDataEx", issuerDataEx)
B.putByteArray("IssuerDataExSignature", issuerDataExSignature)
B.putInt("IssuerDataExCounter", issuerDataExCounter)
}
B.putInt("ProductMask",productMask)
if (isCodeConfirmed != null)
B.putBoolean("codeConfirmed", isCodeConfirmed)
@ -163,6 +182,11 @@ fun TangemCard.saveToBundle(B: Bundle) {
B.putByteArray("terminalPublicKey", terminalPublicKey)
B.putBoolean("terminalIsLinked", terminalIsLinked)
if (tagSignature != null) B.putByteArray("tagSignature", tagSignature)
if (idHash != null) B.putByteArray("idHash", idHash)
if (tlvIDCardData != null) B.putSerializable("tlvIDCardData", tlvIDCardData)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<vector android:height="36dp" android:tint="#505050"
android:viewportHeight="36.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>