Updated on 2026-08-14
This commit is contained in:
commit
4f0b4f3550
17 changed files with 1004 additions and 70 deletions
|
|
@ -23,12 +23,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"),
|
||||
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");
|
||||
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;
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
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
|
||||
)
|
||||
|
|
@ -35,6 +35,9 @@ 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
|
||||
|
|
@ -127,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)
|
||||
}
|
||||
|
|
@ -170,7 +173,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
|
|||
return
|
||||
}
|
||||
|
||||
parseNfcvTag(tag)
|
||||
NfcV.get(tag)?.let { onNfcVDiscovered(it, tag.id) }
|
||||
|
||||
try {
|
||||
// get IsoDep handle and run cardReader thread
|
||||
|
|
@ -195,74 +198,41 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
|
|||
}
|
||||
}
|
||||
|
||||
private fun parseNfcvTag(tag: Tag) {
|
||||
if (NfcV.get(tag) != null) {
|
||||
if (Ndef.get(tag) != null) {
|
||||
try {
|
||||
onNdefDiscovered(Ndef.get(tag), tag.id)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
(activity as MainActivity).nfcManager.notifyReadResult(false)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
(activity as MainActivity).nfcManager.notifyReadResult(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNdefDiscovered(ndef: Ndef, uid: ByteArray) {
|
||||
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) {
|
||||
|
||||
try {
|
||||
ndef.connect()
|
||||
val records = ndef.ndefMessage.records
|
||||
for (record in records) {
|
||||
if (record.toUri() != null) {
|
||||
when (record.toUri().toString()) {
|
||||
"vnd.android.nfc://ext/tangem.com:wallet" -> {
|
||||
//mConsole.write(Util.bytesToHex(record.getPayload()), MessageAdapter.MSG_OKAY, "", null, false);
|
||||
val payload = record.payload
|
||||
Log.v(TAG, "tangem.com:wallet[${payload.size} bytes]:")
|
||||
try {
|
||||
val tlvNDEF: TLVList = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size))
|
||||
val cardDataTlv = TLVList.fromBytes((tlvNDEF.getTLV(TLV.Tag.TAG_CardData)).Value)
|
||||
Log.v(TAG, "\n" + tlvNDEF.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 = tlvNDEF.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value
|
||||
card.status = TangemCard.Status.Loaded
|
||||
card.tagSignature = tlvNDEF.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)
|
||||
}
|
||||
}
|
||||
else -> Log.v(TAG, record.toUri().toString())
|
||||
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)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
(activity as MainActivity).nfcManager.notifyReadResult(false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,8 +286,13 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
|
|||
}
|
||||
}
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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
|
||||
|
|
@ -52,6 +53,7 @@ object CoinEngineFactory {
|
|||
Blockchain.StellarTag -> XlmTagEngine()
|
||||
Blockchain.Eos -> EosEngine()
|
||||
Blockchain.Ducatus -> DucatusEngine()
|
||||
Blockchain.Tezos -> TezosEngine()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +95,8 @@ object CoinEngineFactory {
|
|||
EosEngine(context)
|
||||
else if (Blockchain.Ducatus == context.blockchain)
|
||||
DucatusEngine(context)
|
||||
else if (Blockchain.Tezos == context.blockchain)
|
||||
TezosEngine(context)
|
||||
else
|
||||
return null
|
||||
} catch (e: Exception) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
507
app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt
Normal file
507
app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
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 &&
|
||||
balance!!.notZero()
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ buildscript {
|
|||
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'
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ public interface NfcReader {
|
|||
|
||||
void connect();
|
||||
|
||||
boolean isConnected();
|
||||
}
|
||||
|
|
@ -69,4 +69,20 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb
|
|||
|
||||
// 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_NFC_V or
|
||||
NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
|
||||
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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue