Updated on 2026-08-14
This commit is contained in:
commit
4d10bc64e9
99 changed files with 6022 additions and 602 deletions
195
.idea/navEditor.xml
generated
195
.idea/navEditor.xml
generated
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
25
app/src/main/java/com/tangem/data/network/DucatusApi.java
Normal file
25
app/src/main/java/com/tangem/data/network/DucatusApi.java
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
139
app/src/main/java/com/tangem/data/network/ServerApiTezos.java
Normal file
139
app/src/main/java/com/tangem/data/network/ServerApiTezos.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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/";
|
||||
}
|
||||
35
app/src/main/java/com/tangem/data/network/TezosApi.java
Normal file
35
app/src/main/java/com/tangem/data/network/TezosApi.java
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
25
app/src/main/java/com/tangem/data/network/model/TezosBody.kt
Normal file
25
app/src/main/java/com/tangem/data/network/model/TezosBody.kt
Normal 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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
462
app/src/main/java/com/tangem/ui/fragment/id/IdFragment.kt
Normal file
462
app/src/main/java/com/tangem/ui/fragment/id/IdFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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("###.###.###-##")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
292
app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt
Normal file
292
app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt
Normal 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
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
app/src/main/java/com/tangem/wallet/btc/Unspents.kt
Normal file
3
app/src/main/java/com/tangem/wallet/btc/Unspents.kt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.wallet.btc
|
||||
|
||||
data class Unspents(val unspetns: Int, val gatheredUnspents: Int)
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
68
app/src/main/java/com/tangem/wallet/ethID/EthIdData.java
Normal file
68
app/src/main/java/com/tangem/wallet/ethID/EthIdData.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
534
app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java
Normal file
534
app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java
Normal 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, "", "");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
49
app/src/main/java/com/tangem/wallet/tezos/TezosData.kt
Normal file
49
app/src/main/java/com/tangem/wallet/tezos/TezosData.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
506
app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt
Normal file
506
app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
133
app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt
Normal file
133
app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
423
app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt
Normal file
423
app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt
Normal 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"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
10
app/src/main/res/drawable/ic_add_a_photo.xml
Normal file
10
app/src/main/res/drawable/ic_add_a_photo.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_flip_camera.xml
Normal file
9
app/src/main/res/drawable/ic_flip_camera.xml
Normal 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>
|
||||
4
app/src/main/res/drawable/ic_person.xml
Normal file
4
app/src/main/res/drawable/ic_person.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_take_picture.xml
Normal file
9
app/src/main/res/drawable/ic_take_picture.xml
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
51
app/src/main/res/layout/fragment_camera.xml
Normal file
51
app/src/main/res/layout/fragment_camera.xml
Normal 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>
|
||||
254
app/src/main/res/layout/fragment_id.xml
Normal file
254
app/src/main/res/layout/fragment_id.xml
Normal 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>
|
||||
144
app/src/main/res/layout/fragment_issue_new_id.xml
Normal file
144
app/src/main/res/layout/fragment_issue_new_id.xml
Normal 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>
|
||||
9
app/src/main/res/layout/fragment_tag.xml
Normal file
9
app/src/main/res/layout/fragment_tag.xml
Normal 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>
|
||||
51
app/src/main/res/layout/fragment_validate_id.xml
Normal file
51
app/src/main/res/layout/fragment_validate_id.xml
Normal 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>
|
||||
61
app/src/main/res/layout/fragment_write_id.xml
Normal file
61
app/src/main/res/layout/fragment_write_id.xml
Normal 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>
|
||||
60
app/src/main/res/layout/layout_id.xml
Normal file
60
app/src/main/res/layout/layout_id.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 n’avez 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 l’utilisateur</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
server-android/src/main/res/drawable/card_bc00.png
Normal file
BIN
server-android/src/main/res/drawable/card_bc00.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
server-android/src/main/res/drawable/card_tgslix.png
Normal file
BIN
server-android/src/main/res/drawable/card_tgslix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 issuer’s 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
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ public interface NfcReader {
|
|||
|
||||
void connect();
|
||||
|
||||
boolean isConnected();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ public class VerifyCardTask extends CustomReadCardTask {
|
|||
}
|
||||
mNotifications.onReadProgress(protocol, 90);
|
||||
|
||||
if( protocol.getCard().isIDCard() )
|
||||
{
|
||||
protocol.run_GetIssuerDataEx(mNotifications);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) {
|
|||
Pause(0x1C),
|
||||
|
||||
ManufactureId(0x20),
|
||||
ManufacturerSignature(0x21),
|
||||
ManufacturerSignature(0x86),
|
||||
|
||||
IssuerDataPublicKey(0x30),
|
||||
IssuerTransactionPublicKey(0x31),
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,4 +38,8 @@ data class NfcReader(
|
|||
isoDep.timeout = timeout
|
||||
}
|
||||
|
||||
override fun isConnected(): Boolean {
|
||||
return isoDep.isConnected
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
6
tangem-sdk-old/src/main/res/drawable/id_card_default.xml
Normal file
6
tangem-sdk-old/src/main/res/drawable/id_card_default.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue