Updated on 2026-08-14

This commit is contained in:
Tangem 2019-02-27 21:44:59 +03:00
parent 7c4cc8fcde
commit 85f9a17bb0
245 changed files with 401 additions and 18 deletions

View file

@ -0,0 +1,17 @@
package com.tangem.tangemserver;
public class Server {
/**
* https://tangem-webapp.appspot.com/
* https://tangem-services.appspot.com/
*/
public static class ApiTangem {
public static final String URL_TANGEM = ServerURL.API_TANGEM;
public static class Method {
static final String VERIFY = URL_TANGEM + "verify";
static final String VERIFY_AND_GET_INFO = URL_TANGEM + "card/verify-and-get-info";
static final String ARTWORK = URL_TANGEM + "card/artwork";
}
}
}

View file

@ -0,0 +1,124 @@
package com.tangem.tangemserver;
import androidx.annotation.NonNull;
import android.util.Log;
import com.tangem.tangemcommon.data.TangemCard;
import com.tangem.tangemserver.model.CardVerifyAndGetInfo;
import com.tangem.tangemcommon.util.Util;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class ServerApiTangem {
private static String TAG = ServerApiTangem.class.getSimpleName();
/**
* HTTP
* Card verify
*/
private CardVerifyAndGetInfoListener cardVerifyAndGetInfoListener;
public interface CardVerifyAndGetInfoListener {
void onSuccess(CardVerifyAndGetInfo.Response cardVerifyAndGetArtworkResponse);
void onFail(String message);
}
public void setCardVerifyAndGetInfoListener(CardVerifyAndGetInfoListener listener) {
cardVerifyAndGetInfoListener = listener;
}
public void cardVerifyAndGetInfo(TangemCard card) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://estimatefee.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
TangemApi tangemApi = retrofit.create(TangemApi.class);
List<CardVerifyAndGetInfo.Request.Item> requests = new ArrayList<>();
requests.add(new CardVerifyAndGetInfo.Request.Item(Util.bytesToHex(card.getCID()), Util.bytesToHex(card.getCardPublicKey())));
CardVerifyAndGetInfo.Request requestBody = new CardVerifyAndGetInfo.Request(requests);
Call<CardVerifyAndGetInfo.Response> call = tangemApi.getCardVerifyAndGetInfo(requestBody);
call.enqueue(new Callback<CardVerifyAndGetInfo.Response>() {
@Override
public void onResponse(@NonNull Call<CardVerifyAndGetInfo.Response> call, @NonNull Response<CardVerifyAndGetInfo.Response> response) {
if (response.code() == 200) {
CardVerifyAndGetInfo.Response cardVerifyAndGetArtworkResponse = response.body();
cardVerifyAndGetInfoListener.onSuccess(cardVerifyAndGetArtworkResponse);
Log.i(TAG, "cardVerifyAndGeInfo onResponse " + response.code());
} else {
cardVerifyAndGetInfoListener.onFail(String.valueOf(response.code()));
Log.e(TAG, "cardVerifyAndGetInfo onResponse " + response.code());
}
}
@Override
public void onFailure(@NonNull Call<CardVerifyAndGetInfo.Response> call, @NonNull Throwable t) {
cardVerifyAndGetInfoListener.onFail(t.getMessage());
Log.e(TAG, "cardVerifyAndGetInfo onFailure " + t.getMessage());
}
});
}
/**
* HTTP
* Last version request from GitHub
*/
private ArtworkListener artworkListener;
public interface ArtworkListener {
void onSuccess(String artworkId, InputStream inputStream, Date updateDate);
void onFail(String message);
}
public void setArtworkListener(ArtworkListener listener) {
artworkListener = listener;
}
public void requestArtwork(final String artworkId, final Date updateDate, TangemCard card) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://verify.tangem.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
TangemApi tangemApi = retrofit.create(TangemApi.class);
Call<ResponseBody> call = tangemApi.getArtwork(artworkId, Util.bytesToHex(card.getCID()), Util.bytesToHex(card.getCardPublicKey()));
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) {
Log.i(TAG, "getArtwork onResponse " + response.code());
if (response.code() == 200) {
try {
ResponseBody body = response.body();
if (body != null) {
artworkListener.onSuccess(artworkId, body.byteStream(), updateDate);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
artworkListener.onFail(t.getMessage());
Log.e(TAG, "getArtwork onFailure " + t.getMessage());
}
});
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.tangemserver;
class ServerURL {
static final String API_TANGEM = "https://verify.tangem.com/";
}

View file

@ -0,0 +1,21 @@
package com.tangem.tangemserver;
import com.tangem.tangemserver.model.CardVerifyAndGetInfo;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface TangemApi {
@Headers("Content-Type: application/json")
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO)
Call<CardVerifyAndGetInfo.Response> getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody);
@GET(Server.ApiTangem.Method.ARTWORK)
Call<ResponseBody> getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey);
}

View file

@ -0,0 +1,317 @@
package com.tangem.tangemserver.data
import android.content.Context
import com.tangem.tangemcommon.data.TangemCard
import android.annotation.SuppressLint
import android.graphics.Bitmap
import java.io.File
import kotlin.collections.HashMap
import android.graphics.BitmapFactory
import android.util.Log
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.tangem.tangemserver.model.CardVerifyAndGetInfo
import com.tangem.tangemserver.R
import com.tangem.tangemcommon.data.external.CardDataSubstitutionProvider
import com.tangem.tangemcommon.reader.CardCrypto
import com.tangem.tangemcommon.util.Util
import java.io.InputStream
import java.lang.Exception
import java.nio.charset.StandardCharsets
import java.util.*
class LocalStorage
(
val context: Context
) : CardDataSubstitutionProvider {
private lateinit var artworks: HashMap<String, ArtworkInfo>
private lateinit var batches: HashMap<String, BatchInfo>
private val artworksFile: File = File(context.filesDir, "artworks.json")
private val batchesFile: File = File(context.filesDir, "batches.json")
private var cacheDir: File? = null
init {
cacheDir = File(context.filesDir, "artworks")
if (!cacheDir!!.exists())
cacheDir!!.mkdirs()
if (artworksFile.exists()) {
try {
artworksFile.bufferedReader().use { artworks = Gson().fromJson(it, object : TypeToken<HashMap<String, ArtworkInfo>>() {}.type) }
} catch (e: Exception) {
e.printStackTrace()
artworks = HashMap()
}
} else {
artworks = HashMap()
}
if (artworks.count() < 21) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
putResourceArtworkToCatalog(R.drawable.card_ru006, false)
putResourceArtworkToCatalog(R.drawable.card_ru007, false)
putResourceArtworkToCatalog(R.drawable.card_ru011, false)
putResourceArtworkToCatalog(R.drawable.card_ru012, false)
putResourceArtworkToCatalog(R.drawable.card_ru013, false)
putResourceArtworkToCatalog(R.drawable.card_ru014, false)
putResourceArtworkToCatalog(R.drawable.card_ru015, false)
putResourceArtworkToCatalog(R.drawable.card_ru016, false)
putResourceArtworkToCatalog(R.drawable.card_ru020, false)
putResourceArtworkToCatalog(R.drawable.card_ru021, false)
putResourceArtworkToCatalog(R.drawable.card_ru022, false)
putResourceArtworkToCatalog(R.drawable.card_ru023, false)
putResourceArtworkToCatalog(R.drawable.card_ru024, false)
putResourceArtworkToCatalog(R.drawable.card_ru028, false)
putResourceArtworkToCatalog(R.drawable.card_ru029, false)
putResourceArtworkToCatalog(R.drawable.card_ru030, false)
putResourceArtworkToCatalog(R.drawable.card_ru031, false)
putResourceArtworkToCatalog(R.drawable.card_ru032, false)
putResourceArtworkToCatalog(R.drawable.card_ff32, true)
}
if (batchesFile.exists()) {
try {
batchesFile.bufferedReader().use { batches = Gson().fromJson(it, object : TypeToken<HashMap<String, BatchInfo>>() {}.type) }
} catch (e: Exception) {
e.printStackTrace()
batches = HashMap()
}
} else {
batches = HashMap()
}
}
private fun getArtworkFile(artworkId: String): File {
return File(cacheDir, "$artworkId.png")
}
private fun loadArtworkBitmapFromFile(artworkId: String): Bitmap? {
return try {
BitmapFactory.decodeFile(getArtworkFile(artworkId).absolutePath)
} catch (E: Exception) {
E.printStackTrace()
null
}
}
private fun saveArtworkBitmapToFile(artworkId: String, data: ByteArray) {
val file = getArtworkFile(artworkId)
file.writeBytes(data)
}
fun checkNeedUpdateArtwork(artwork: CardVerifyAndGetInfo.Response.Item.ArtworkInfo?): Boolean {
if (artwork == null || artwork.id.isBlank()) return false
val localArtwork = artworks[artwork.id] ?: return true
if (localArtwork.hash == artwork.hash) return false
return localArtwork.updateDate == null || localArtwork.updateDate.before(artwork.getUpdateDate())
}
fun checkBatchInfoChanged(card: TangemCard, result: CardVerifyAndGetInfo.Response.Item): Boolean {
var sData: String? = result.substitution?.data
var sSignature: String? = result.substitution?.signature
if (card.batch != result.batch) {
Log.e("CardDataSubstitution", "Invalid batch received!")
return false
}
if (!BatchInfo.CardDataSubstitution.verifySignature(card, sData, sSignature)) {
sData = null
sSignature = null
}
val batchInfo = batches[result.batch]
if (batchInfo == null || batchInfo.artworkId?.toLowerCase() != result.artwork?.id?.toLowerCase() || batchInfo.dataSubstitution != sData) {
putBatchToCatalog(result.batch, result.artwork?.id?.toLowerCase(), sData, sSignature)
return true
}
return false
}
fun updateArtwork(artworkId: String, inputStream: InputStream, updateDate: Date) {
val data = inputStream.readBytes()
saveArtworkBitmapToFile(artworkId.toLowerCase(), data)
putArtworkToCatalog(artworkId, false, data, updateDate, true)
}
// private fun putBatchToCatalog(batch: String, resourceId: Int, forceSave: Boolean = true) {
// putBatchToCatalog(batch, context.resources.getResourceEntryName(resourceId), null, null, forceSave)
// }
private fun putBatchToCatalog(batch: String, artworkId: String?, substitution: String?, substitutionSignature: String?, forceSave: Boolean = true) {
batches[batch] = BatchInfo(artworkId, substitution, substitutionSignature)
if (forceSave) {
val sBatches = Gson().toJson(batches)
batchesFile.bufferedWriter().use { it.write(sBatches) }
}
}
@SuppressLint("ResourceType")
private fun putResourceArtworkToCatalog(resourceId: Int, forceSave: Boolean) {
context.resources.openRawResource(R.drawable.card_default).use { putArtworkToCatalog(context.resources.getResourceEntryName(resourceId), true, it.readBytes(), null, forceSave) }
}
private fun putArtworkToCatalog(artworkId: String, isResource: Boolean, data: ByteArray, updateDate: Date?, forceSave: Boolean = true) {
val artworkInfo = ArtworkInfo(
isResource, Util.bytesToHex(Util.calculateSHA256(data)), updateDate
)
artworks[artworkId.toLowerCase()] = artworkInfo
if (forceSave) {
val sArtworks = Gson().toJson(artworks)
artworksFile.bufferedWriter().use { it.write(sArtworks) }
}
}
private fun getArtworkBitmap(artworkId: String): Bitmap? {
if (artworkId.isBlank()) return null
val info = artworks[artworkId.toLowerCase()] ?: return null
return if (info.isResource) {
val resID = context.resources.getIdentifier(artworkId, "drawable", context.packageName)
if (resID == 0) return null
BitmapFactory.decodeResource(context.resources, resID)
} else {
loadArtworkBitmapFromFile(artworkId)
}
}
private fun getDefaultArtworkBitmap(card: TangemCard): Bitmap {
val defaultArtworkId: String
if (card.tokenSymbol.startsWith("NFT:")) {
defaultArtworkId = context.resources.getResourceEntryName(R.drawable.card_default_nft)
} else {
defaultArtworkId = context.resources.getResourceEntryName(R.drawable.card_default)
}
val info = artworks[defaultArtworkId]
var bitmap: Bitmap? = null
if (info != null && !info.isResource) {
bitmap = loadArtworkBitmapFromFile(defaultArtworkId)
}
if (bitmap == null) {
if (card.tokenSymbol.startsWith("NFT:")) {
return BitmapFactory.decodeResource(context.resources, R.drawable.card_default_nft)
} else {
return BitmapFactory.decodeResource(context.resources, R.drawable.card_default)
}
}
return bitmap
}
fun getCardArtworkBitmap(card: TangemCard): Bitmap {
// special cases (first series of cards, hardcode CID->artwork), on new series batch<->artwork
val hexCID = Util.bytesToHex(card.cid)
val artworkResourceId: Int? = when {
hexCID in "AA01000000000000".."AA01000000004999" -> R.drawable.card_ru006
hexCID in "AA01000000005000".."AA01000000009999" -> R.drawable.card_ru007
hexCID in "AE01000000000000".."AE01000000004999" -> R.drawable.card_ru006
hexCID in "AE01000000005000".."AE01000000009999" -> R.drawable.card_ru007
hexCID in "CB01000000000000".."CB01000000009999" -> R.drawable.card_ru006
hexCID in "CB01000000010000".."CB01000000019999" -> R.drawable.card_ru007
hexCID in "CB01000000020000".."CB01000000039999" -> R.drawable.card_ru006
hexCID in "CB01000000040000".."CB01000000059999" -> R.drawable.card_ru007
hexCID in "CB02000000000000".."CB02000000024999" -> R.drawable.card_ru006
hexCID in "CB02000000025000".."CB02000000049999" -> R.drawable.card_ru007
hexCID in "CB05000010000000".."CB05000010009999" -> R.drawable.card_ru006
card.batch == "0004" -> R.drawable.card_ru006
card.batch == "0006" -> R.drawable.card_ru006
card.batch == "0010" -> R.drawable.card_ru006
card.batch == "0005" -> R.drawable.card_ru007
card.batch == "0007" -> R.drawable.card_ru007
card.batch == "0011" -> R.drawable.card_ru007
card.batch == "0012" -> R.drawable.card_ru011
card.batch == "0013" -> R.drawable.card_ru012
card.batch == "0014" -> R.drawable.card_ru006
card.batch == "0015" -> R.drawable.card_ru020
card.batch == "0016" -> R.drawable.card_ru021
card.batch == "0017" -> R.drawable.card_ru013
card.batch == "0019" -> R.drawable.card_ru016
card.batch == "001A" -> R.drawable.card_ru014
card.batch == "001B" -> R.drawable.card_ru015
card.batch == "001C" -> R.drawable.card_ru023
card.batch == "001D" -> R.drawable.card_ru022
card.batch == "001E" -> R.drawable.card_ru024
card.batch == "001F" -> R.drawable.card_ru028
card.batch == "0018" -> R.drawable.card_ru029
card.batch == "0020" -> R.drawable.card_ru030
card.batch == "0021" -> R.drawable.card_ru031
card.batch == "0022" -> R.drawable.card_ru032
card.batch == "FF32" -> R.drawable.card_ff32
else -> null
}
if (artworkResourceId != null) {
val artworkId = context.resources.getResourceEntryName(artworkResourceId)
return getArtworkBitmap(artworkId) ?: return getDefaultArtworkBitmap(card)
}
val batchInfo = batches[card.batch] ?: return getDefaultArtworkBitmap(card)
val artworkId = batchInfo.artworkId ?: return getDefaultArtworkBitmap(card)
return getArtworkBitmap(artworkId) ?: return getDefaultArtworkBitmap(card)
}
override fun applySubstitution(card: TangemCard) {
val batchInfo = batches[card.batch] ?: return
val substitution = batchInfo.getDataSubstitution(card) ?: return
substitution.applyToCard(card)
}
private data class ArtworkInfo(
val isResource: Boolean,
val hash: String,
val updateDate: Date?
)
private data class BatchInfo(
val artworkId: String?,
val dataSubstitution: String?,
val dataSubstitutionSignature: String?
) {
fun getDataSubstitution(card: TangemCard): CardDataSubstitution? {
return if (CardDataSubstitution.verifySignature(card, dataSubstitution, dataSubstitutionSignature)) {
Gson().fromJson(dataSubstitution, CardDataSubstitution::class.java)
} else {
null
}
}
class CardDataSubstitution(
@SerializedName("token_symbol")
private val tokenSymbol: String?,
@SerializedName("token_decimal")
private val tokenDecimal: Int?,
@SerializedName("token_contract_address")
private val contractAddress: String?
) {
fun applyToCard(card: TangemCard) {
if (tokenSymbol != null && (card.tokenSymbol.isNullOrBlank() || card.tokenSymbol.toLowerCase() == "not defined")) card.tokenSymbol = tokenSymbol
if (tokenDecimal != null && card.tokensDecimal == 0) card.tokensDecimal = tokenDecimal
if (contractAddress != null && (card.contractAddress.isNullOrBlank() || card.contractAddress.toLowerCase() == "not defined")) card.contractAddress = contractAddress
}
companion object {
fun verifySignature(card: TangemCard, substitutionData: String?, substitutionSignature: String?): Boolean {
if (substitutionData == null && substitutionSignature == null) return true
if (substitutionData == null || substitutionSignature == null) return false
val dataToSign = card.batch.toByteArray(StandardCharsets.UTF_8) + substitutionData.toByteArray(StandardCharsets.UTF_8)
return try {
CardCrypto.VerifySignature(card.issuerPublicDataKey, dataToSign, Util.hexToBytes(substitutionSignature))
} catch (E: Exception) {
E.printStackTrace()
false
}
}
}
}
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.tangemserver.model;
public class CardVerify {
private String CID;
private String publicKey;
public CardVerify(String CID, String publicKey) {
this.CID = CID;
this.publicKey = publicKey;
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.tangemserver.model
import java.text.SimpleDateFormat
import java.util.*
class CardVerifyAndGetInfo {
data class Request(
var requests: List<Item>? = null
) {
data class Item(
var CID: String = "",
var publicKey: String = ""
)
}
data class Response(
var results: List<Item>? = null
) {
data class Item(
var error: String? = null,
var CID: String = "",
var passed: Boolean = false,
var batch: String = "",
var artwork: ArtworkInfo? = null,
var substitution: SubstitutionInfo? = null
) {
data class ArtworkInfo(
var id: String = "",
var hash: String = "",
var date: String = ""
) {
fun getUpdateDate(): Date? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US).parse(date)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
data class SubstitutionInfo(
var data: String? = null,
var signature: String? = null
)
}
}
}