Updated on 2026-08-14

This commit is contained in:
Tangem 2020-03-05 11:42:24 +03:00
commit e840e5b224
12 changed files with 150 additions and 318 deletions

2
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,2 @@
# Default ignored files
/shelf/

View file

@ -503,8 +503,12 @@ public class BtcCashEngine extends CoinEngine {
if (addressData.getAddress().getBalance() != 0) {
blockchainRequestsCallbacks.onComplete(true);
} else {
requestIsTransactionConfirmed(addressData.getTransactions().get(0), blockchainRequestsCallbacks);
} else { //check if there is an unconfirmed tx, which spent all funds
if (addressData.getTransactions().isEmpty()) {
blockchainRequestsCallbacks.onComplete(true);
} else {
requestIsTransactionConfirmed(addressData.getTransactions().get(0), blockchainRequestsCallbacks);
}
}
} catch (Exception e) {
e.printStackTrace();

View file

@ -8,12 +8,10 @@ import com.tangem.App;
import com.tangem.data.local.PendingTransactionsStorage;
import com.tangem.data.network.Server;
import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiSoChain;
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.SoChain;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
@ -33,8 +31,6 @@ import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONException;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -505,268 +501,121 @@ public class LtcEngine extends BtcEngine {
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) throws Exception {
ctx.setError(null);
if (!coinData.isUseBlockcypher()) {
final ServerApiSoChain serverApiSoChain = new ServerApiSoChain();
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiSoChain.AddressInfoListener addressInfoListener = new ServerApiSoChain.AddressInfoListener() {
@Override
public void onSuccess(SoChain.Response.AddressBalance response) {
try {
String walletAddress = response.getData().getAddress();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
Long confBalance = convertToInternalAmount(convertToAmount(response.getData().getConfirmed_balance(), getBalanceCurrency())).longValueExact();
Long unconfirmedBalance = convertToInternalAmount(convertToAmount(response.getData().getUnconfirmed_balance(), getBalanceCurrency())).longValueExact();
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(confBalance);
coinData.setBalanceUnconfirmed(unconfirmedBalance);
coinData.setValidationNodeDescription(Server.ApiSoChain.URL);
} catch (JSONException e) {
e.printStackTrace();
Log.e(TAG, "FAIL METHOD_GetBalance JSONException");
ctx.setError("FAIL METHOD_GetBalance JSONException");
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL METHOD_GetBalance Exception");
ctx.setError("FAIL METHOD_GetBalance Exception");
ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() {
@Override
public void onSuccess(String method, BlockcypherResponse blockcypherResponse) {
Log.i(TAG, "onSuccess: " + method);
try {
String walletAddress = blockcypherResponse.getAddress();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
if (serverApiSoChain.isRequestsSequenceCompleted()) {
if (!coinData.isUseBlockcypher()) {
checkPending(blockchainRequestsCallbacks);
} else {
try {
requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks);
} catch (Exception e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
Long confBalance = blockcypherResponse.getBalance();
Long unconfirmedBalance = blockcypherResponse.getUnconfirmed_balance();
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(confBalance);
coinData.setBalanceUnconfirmed(unconfirmedBalance);
coinData.setValidationNodeDescription(Server.ApiBlockcypher.URL_BLOCKCYPHER);
coinData.getUnspentTransactions().clear();
//TODO: change request logic, 2000 tx max
if (blockcypherResponse.getTxrefs() != null) {
for (BlockcypherTxref txref : blockcypherResponse.getTxrefs()) {
if (txref.getTx_input_n() == -1) { //recieved only
if (!txref.getSpent()) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = txref.getTx_hash();
trUnspent.amount = txref.getValue();
trUnspent.outputN = txref.getTx_output_n();
trUnspent.script = txref.getScript();
coinData.getUnspentTransactions().add(trUnspent);
}
} else { //sent only
coinData.incSentTransactionsCount();
}
}
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onSuccess(SoChain.Response.TxUnspent response) {
String walletAddress = response.getData().getAddress();
try {
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
coinData.getUnspentTransactions().clear();
if (response.getData().getTxs() != null)
for (SoChain.Response.TxUnspent.Data.Tx tx : response.getData().getTxs()) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = tx.getTxid();
trUnspent.amount = convertToInternalAmount(convertToAmount(tx.getValue(), getBalanceCurrency())).longValueExact();
trUnspent.outputN = tx.getOutput_no();
trUnspent.script = tx.getScript_hex();
coinData.getUnspentTransactions().add(trUnspent);
// if (blockchainRequestsCallbacks.allowAdvance()) {
// //serverApiSoChain.requestData(ctx, ElectrumRequest.getTransaction(walletAddress, hash));
// } else {
// ctx.setError("Terminated by user");
// }
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL METHOD_ListUnspent JSONException");
}
if (serverApiSoChain.isRequestsSequenceCompleted()) {
if (!coinData.isUseBlockcypher()) {
checkPending(blockchainRequestsCallbacks);
} else {
try {
requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks);
} catch (Exception e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
if (blockcypherResponse.getUnconfirmed_txrefs() != null) {
for (BlockcypherTxref unconfirmedTxref : blockcypherResponse.getUnconfirmed_txrefs()) {
if (unconfirmedTxref.getTx_input_n() != -1) {
coinData.incSentTransactionsCount();
}
}
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onFail(String message) {
Log.i(TAG, "onFail: " + message);
coinData.setUseBlockcypher(true);
// ctx.setError(R.string.cannot_obtain_data_from_blockchain);
if (serverApiSoChain.isRequestsSequenceCompleted()) {
// blockchainRequestsCallbacks.onComplete(false);//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError());
try {
requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks);
} catch (Exception e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
} else {
blockchainRequestsCallbacks.onProgress();
}
}
};
serverApiSoChain.setAddressInfoListener(addressInfoListener);
serverApiSoChain.requestAddressBalance(ctx.getBlockchain(), coinData.getWallet());
serverApiSoChain.requestUnspentTx(ctx.getBlockchain(), coinData.getWallet());
} else {
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 {
String walletAddress = blockcypherResponse.getAddress();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
Long confBalance = blockcypherResponse.getBalance();
Long unconfirmedBalance = blockcypherResponse.getUnconfirmed_balance();
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(confBalance);
coinData.setBalanceUnconfirmed(unconfirmedBalance);
coinData.setValidationNodeDescription(Server.ApiBlockcypher.URL_BLOCKCYPHER);
coinData.getUnspentTransactions().clear();
if (blockcypherResponse.getTxrefs() != null) {
for (BlockcypherTxref txref : blockcypherResponse.getTxrefs()) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = txref.getTx_hash();
trUnspent.amount = txref.getValue();
trUnspent.outputN = txref.getTx_output_n();
trUnspent.script = txref.getScript();
coinData.getUnspentTransactions().add(trUnspent);
}
}
} catch (Exception e) {
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
e.printStackTrace();
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
checkPending(blockchainRequestsCallbacks);
}
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
ctx.setError("Wrong response type for requestBalanceAndUnspentTransactions");
} catch (Exception e) {
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
e.printStackTrace();
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
};
checkPending(blockchainRequestsCallbacks);
}
serverApiBlockcypher.setResponseListener(blockcypherListener);
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
ctx.setError("Wrong response type for requestBalanceAndUnspentTransactions");
blockchainRequestsCallbacks.onComplete(false);
}
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
}
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiBlockcypher.setResponseListener(blockcypherListener);
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
}
private void checkPending(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
if (App.pendingTransactionsStorage.hasTransactions(ctx.getCard())) {
ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
if (!coinData.isUseBlockcypher()) {
ServerApiSoChain serverApiSoChain = new ServerApiSoChain();
ServerApiSoChain.TransactionInfoListener listener = new ServerApiSoChain.TransactionInfoListener() {
@Override
public void onSuccess(SoChain.Response.GetTx response) {
Log.i(TAG, "onSuccess: GetTx");
try {
if (response.getData() != null && response.getData().getTx_hex() != null && response.getData().getTxid() != null) {
App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), response.getData().getTx_hex());
}
} catch (Exception e) {
Log.e(TAG, "onFail: GetTx" + response);
}
if (serverApiSoChain.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onFail(String message) {
Log.i(TAG, "onFail: GetTx " + message);
if (serverApiSoChain.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
};
serverApiSoChain.setTransactionInfoListener(listener);
for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) {
String txId = BTCUtils.toHex(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(pendingTx.getTx()))));
ServerApiBlockcypher.TxResponseListener listener = new ServerApiBlockcypher.TxResponseListener() {
@Override
public void onSuccess(BlockcypherTx response) {
Log.i(TAG, "onSuccess: BlockcypherTx");
try {
serverApiSoChain.requestTransactionInfo(ctx.getBlockchain(), txId);
if (response.getHex() != null) {
App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), response.getHex());
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "onFail: BlockcypherTx" + response);
}
if (serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
} else {
ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiBlockcypher.TxResponseListener listener = new ServerApiBlockcypher.TxResponseListener() {
@Override
public void onSuccess(BlockcypherTx response) {
Log.i(TAG, "onSuccess: BlockcypherTx");
try {
if (response.getHex() != null) {
App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), response.getHex());
}
} 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.setTxResponseListener(listener);
@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.setTxResponseListener(listener);
for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) {
String txId = BTCUtils.toHex(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(pendingTx.getTx()))));
try {
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txId);
} catch (Exception e) {
e.printStackTrace();
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
}
for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) {
String txId = BTCUtils.toHex(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(pendingTx.getTx()))));
try {
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_TXS, "", txId);
} catch (Exception e) {
e.printStackTrace();
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
}
}
} else {
@ -785,87 +634,48 @@ public class LtcEngine extends BtcEngine {
@Override
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) throws Exception {
final String txStr = BTCUtils.toHex(txForSend);
if (!coinData.isUseBlockcypher()) {
final ServerApiSoChain serverApiSoChain = new ServerApiSoChain();
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiSoChain.SendTxListener listener = new ServerApiSoChain.SendTxListener() {
@Override
public void onSuccess(SoChain.Response.SendTx response) {
try {
if (response.getStatus() == null || response.getData() == null) {
ctx.setError("Rejected by node: invalid answer received");
blockchainRequestsCallbacks.onComplete(false);
} else if (response.getStatus().equals("fail") || response.getData().getTxid() == null) {
ctx.setError("Rejected by node: " + response.getData());
blockchainRequestsCallbacks.onComplete(false);
} else {
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);
}
ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() {
@Override
public void onSuccess(String method, BlockcypherResponse blockcypherResponse) {
String resultString = blockcypherResponse.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 onFail(String message) {
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiSoChain.setSendTxListener(listener);
serverApiSoChain.requestSendTransaction(ctx.getBlockchain(), txStr);
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestSendTransaction");
ctx.setError("Wrong response type for requestSendTransaction");
blockchainRequestsCallbacks.onComplete(false);
}
} else {
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiBlockcypher.setResponseListener(blockcypherListener);
ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() {
@Override
public void onSuccess(String method, BlockcypherResponse blockcypherResponse) {
String resultString = blockcypherResponse.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);
}
}
}
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestSendTransaction");
ctx.setError("Wrong response type for requestSendTransaction");
blockchainRequestsCallbacks.onComplete(false);
}
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiBlockcypher.setResponseListener(blockcypherListener);
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_SEND, "", txStr);
}
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_SEND, "", txStr);
}
@Override

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.3.61',
build_gradle: '3.6.0',
build_gradle: '3.6.1',
]

View file

@ -47,7 +47,7 @@ class LocalStorage
artworks = HashMap()
}
if (artworks.count() < 32) {
if (artworks.count() < 39) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
@ -78,6 +78,13 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru043, false)
putResourceArtworkToCatalog(R.drawable.card_tg044, false)
putResourceArtworkToCatalog(R.drawable.card_tg046, false)
putResourceArtworkToCatalog(R.drawable.card_tg054, false)
putResourceArtworkToCatalog(R.drawable.card_tg055, false)
putResourceArtworkToCatalog(R.drawable.card_tg057, false)
putResourceArtworkToCatalog(R.drawable.card_tg060, false)
putResourceArtworkToCatalog(R.drawable.card_tg061, false)
putResourceArtworkToCatalog(R.drawable.card_tg062, false)
putResourceArtworkToCatalog(R.drawable.card_tg063, false)
putResourceArtworkToCatalog(R.drawable.card_tgslix, false)
putResourceArtworkToCatalog(R.drawable.card_bc00, false)
putResourceArtworkToCatalog(R.drawable.card_ff32, true)
@ -274,6 +281,15 @@ class LocalStorage
card.batch == "0029" -> R.drawable.card_ru041
card.batch == "0031" -> R.drawable.card_ru042
card.batch == "0034" -> R.drawable.card_tg046
card.batch == "0041" -> R.drawable.card_tg054
card.batch == "0042" -> R.drawable.card_tg055
card.batch == "0046" -> R.drawable.card_tg054
card.batch == "0047" -> R.drawable.card_tg055
card.batch == "0045" -> R.drawable.card_tg057
card.batch == "0049" -> R.drawable.card_tg060
card.batch == "0050" -> R.drawable.card_tg061
card.batch == "0051" -> R.drawable.card_tg062
card.batch == "0052" -> R.drawable.card_tg063
card.batch == "FF32" -> R.drawable.card_ff32
else -> null

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB