Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-27 16:34:37 +03:00
commit 660cab44c8
161 changed files with 8909 additions and 630 deletions

32
.gitignore vendored
View file

@ -1,20 +1,22 @@
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
# Built application files
/build
/captures
.externalNativeBuild
.idea/vcs.xml
.idea/caches
.idea/dictionaries
.idea/runConfigurations.xml
.idea/encodings.xml
.idea/codeStyles/codeStyleConfig.xml
.idea/assetWizardSettings.xml
# Local configuration file (sdk path, etc)
local.properties
# Gradle generated files
.gradle
# User-specific configurations
.idea/caches/
.idea/libraries/
.idea/*.xml
# OS-specific files
.DS_Store
.DS_Store?
# fastlane files
**/fastlane/report.xml

1
.idea/gradle.xml generated
View file

@ -11,6 +11,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/blockchain" />
<option value="$PROJECT_DIR$/server-android" />
<option value="$PROJECT_DIR$/tangem-card-old" />
<option value="$PROJECT_DIR$/tangem-core" />

View file

@ -125,7 +125,7 @@ dependencies {
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
implementation "com.orhanobut:hawk:2.0.1"
implementation 'co.nstant.in:cbor:0.8'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2"
// implementation 'org.bitcoinj:bitcoinj-parent:0.14.7' //TODO: is this needed?

View file

@ -66,6 +66,16 @@
android:host="app.tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:scheme="vnd.android.nfc"
android:host="ext"
android:pathPrefix="/android.com:pkg"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>

View file

@ -9,6 +9,7 @@ public enum Blockchain {
Unknown("", "", 1.0, R.drawable.ic_logo_unknown, ""),
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"),
BitcoinDual("BTC/dual", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Bitcoin"),
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"),

View file

@ -14,7 +14,7 @@ import retrofit2.http.Query;
public interface BlockchainInfoApi {
@GET(Server.ApiBlockchainInfo.Method.ADDRESS)
Single<BlockchainInfoAddress> blockchainInfoAddress(@Path("address") String address);
Single<BlockchainInfoAddress> blockchainInfoAddress(@Path("address") String address, @Query("offset") Integer offset);
@GET(Server.ApiBlockchainInfo.Method.UTXO)
Single<BlockchainInfoUnspents> blockchainInfoUnspents(@Query("active") String address);

View file

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

View file

@ -113,7 +113,7 @@ public class Server {
public static class Method {
static final String MAIN = URL_BLOCKCYPHER + V1_MAIN;
static final String ADDRESS = MAIN + "/addrs/{address}?unspentOnly=true&includeScript=true";
static final String ADDRESS = MAIN + "/addrs/{address}?includeScript=true&limit=2000";
static final String TXS = MAIN + "/txs/{txHash}?includeHex=true";
static final String PUSH = MAIN + "/txs/push";
}
@ -123,7 +123,7 @@ public class Server {
public static final String URL_BLOCKCHAININFO = ServerURL.API_BLOCKCHAIN_INFO;
public static class Method {
static final String ADDRESS = URL_BLOCKCHAININFO + "rawaddr/{address}?limit=5";
static final String ADDRESS = URL_BLOCKCHAININFO + "rawaddr/{address}";
static final String UTXO = URL_BLOCKCHAININFO + "unspent";
// static final String TX = URL_BLOCKCHAININFO + "rawtx/{txHash}";
static final String PUSH = URL_BLOCKCHAININFO + "pushtx";

View file

@ -5,9 +5,11 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.model.BlockchainInfoAddress;
import com.tangem.data.network.model.BlockchainInfoAddressAndUnspents;
import com.tangem.data.network.model.BlockchainInfoTransaction;
import com.tangem.data.network.model.BlockchainInfoUnspents;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
@ -17,12 +19,13 @@ import okhttp3.ResponseBody;
public class ServerApiBlockchainInfo {
private static String TAG = ServerApiBlockchainInfo.class.getSimpleName();
private int page = 1;
public void getAddressAndUnspents(String wallet, SingleObserver<BlockchainInfoAddressAndUnspents> addressAndUnspentsObserver) {
Log.i(TAG, "new getAddressAndUnspents request");
BlockchainInfoApi api = App.Companion.getNetworkComponent().getRetrofitBlockchainInfo().create(BlockchainInfoApi.class);
Single<BlockchainInfoAddress> addressObservable = api.blockchainInfoAddress(wallet);
Single<BlockchainInfoAddress> addressObservable = api.blockchainInfoAddress(wallet, null);
Single<BlockchainInfoUnspents> unspentsObservable = api.blockchainInfoUnspents(wallet)
.onErrorReturnItem(new BlockchainInfoUnspents(new ArrayList<>()));
@ -43,4 +46,17 @@ public class ServerApiBlockchainInfo {
sendObservable.subscribe(sendObserver);
}
public Single<List<BlockchainInfoTransaction>> getMoreAddressTxs(String wallet) {
Log.i(TAG, "new getAddress request");
BlockchainInfoApi api = App.Companion.getNetworkComponent().getRetrofitBlockchainInfo().create(BlockchainInfoApi.class);
Single<List<BlockchainInfoTransaction>> addressObservable = api.blockchainInfoAddress(wallet, page * 50)
.map(BlockchainInfoAddress::getTxs)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
page++;
return addressObservable;
}
}

View file

@ -32,8 +32,9 @@ public class ServerApiBlockcypher {
return requestsCount <= 0;
}
private ResponseListener responseListener;
private String apiKey = null;
private ResponseListener responseListener;
private TxResponseListener txResponseListener;
public interface ResponseListener {
@ -68,20 +69,29 @@ public class ServerApiBlockcypher {
blockchain = "btc";
network = "test3";
}
if (blockchainID.equals(Blockchain.Token.getID())) blockchain = "eth";
if (blockchainID.equals(Blockchain.BitcoinDual.getID())) blockchain = "btc";
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 +105,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 +136,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 +167,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 +204,9 @@ public class ServerApiBlockcypher {
break;
}
}
private String getRandomApiKey() {
return BlockcypherToken
.values()[new Random().nextInt(BlockcypherToken.values().length)].getToken();
}
}

View file

@ -4,11 +4,16 @@ import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Server;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.requests.ErrorResponse;
import org.stellar.sdk.requests.RequestBuilder;
import org.stellar.sdk.responses.AccountResponse;
import org.stellar.sdk.responses.LedgerResponse;
import org.stellar.sdk.responses.Page;
import org.stellar.sdk.responses.SubmitTransactionResponse;
import org.stellar.sdk.responses.operations.OperationResponse;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
/**
* Created by dvol on 7.01.2019.
@ -72,13 +77,37 @@ public class StellarRequest {
public static class Ledgers extends Base {
public LedgerResponse ledgerResponse;
public Ledgers() {};
public Ledgers() {
}
@Override
public void process(Server server) throws IOException {
int latestLedger = server.root().getCoreLatestLedger();
int latestLedger = server.root().getHistoryLatestLedger();
ledgerResponse = server.ledgers().ledger(latestLedger);
}
}
public static class Operations extends Base {
KeyPair accountKeyPair;
public List<OperationResponse> operationsList;
int limit = 200;
public Operations(String walletAddress) {
accountKeyPair = KeyPair.fromAccountId(walletAddress);
}
@Override
public void process(Server server) throws IOException {
Page<OperationResponse> operationsResponse = server.operations().forAccount(accountKeyPair).limit(limit).order(RequestBuilder.Order.DESC).execute();
operationsList = operationsResponse.getRecords();
while (operationsResponse.getRecords().size() == limit) {
try {
operationsResponse = operationsResponse.getNextPage(server.getHttpClient());
operationsList.addAll(operationsResponse.getRecords());
} catch (URISyntaxException e) {
break;
}
}
}
}
}

View file

@ -15,7 +15,10 @@ data class BlockchainInfoTransaction(
var hash: String? = null,
@SerializedName("block_height")
var block_height: Long? = null
var block_height: Long? = null,
@SerializedName("inputs")
var inputs: List<BlockchainInfoInput>
)
data class BlockchainInfoUnspents(
@ -37,6 +40,16 @@ data class BlockchainInfoUtxo(
var script: String? = null
)
data class BlockchainInfoInput(
@SerializedName("prev_out")
var prev_out: BlockchainInfoOutput
)
data class BlockchainInfoOutput(
@SerializedName("addr")
var addr: String? = null
)
data class BlockchainInfoAddressAndUnspents(
var address: BlockchainInfoAddress,
var unspents: BlockchainInfoUnspents

View file

@ -13,13 +13,22 @@ data class BlockcypherResponse(
var unconfirmed_balance: Long? = null,
@SerializedName("txrefs")
var txrefs: List<BlockcypherTxref>? = null
var txrefs: List<BlockcypherTxref>? = null,
@SerializedName("unconfirmed_txrefs")
var unconfirmed_txrefs: List<BlockcypherTxref>? = null,
@SerializedName("hasMore")
var hasMore: Boolean? = null
)
data class BlockcypherTxref(
@SerializedName("tx_hash")
var tx_hash: String? = null,
@SerializedName("tx_input_n")
var tx_input_n: Int? = null,
@SerializedName("tx_output_n")
var tx_output_n: Int? = null,
@ -30,7 +39,10 @@ data class BlockcypherTxref(
var confirmations: Long? = null,
@SerializedName("script")
var script: String? = null
var script: String? = null,
@SerializedName("spent")
var spent: Boolean? = null
)
data class BlockcypherTx(

View file

@ -32,8 +32,6 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
lateinit var viewModel: GlobalViewModel
lateinit var nfcManager: NfcManager
// private var onNfcReaderCallback: NfcAdapter.ReaderCallback? = null
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action || NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action)) {
@ -68,15 +66,6 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
nfcManager = NfcManager(this, this)
lifecycle.addObserver(NfcLifecycleObserver(nfcManager))
// // NFC
// val intent = intent
// if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action || NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action)) {
// val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
// if (tag != null && onNfcReaderCallback != null) {
// onNfcReaderCallback?.onTagDiscovered(tag)
// }
// }
// check if root device
val rootBeer = RootBeer(this)
if (rootBeer.isRootedWithoutBusyBoxCheck && !BuildConfig.DEBUG)

View file

@ -364,7 +364,7 @@ class IdFragment : BaseFragment(), NfcAdapter.ReaderCallback,
updateViews()
// Bitcoin, Litecoin, BitcoinCash, Stellar
if (ctx.blockchain == Blockchain.Bitcoin || ctx.blockchain == Blockchain.BitcoinTestNet ||
if (ctx.blockchain == Blockchain.Bitcoin || ctx.blockchain == Blockchain.BitcoinTestNet || ctx.blockchain == Blockchain.BitcoinDual ||
ctx.blockchain == Blockchain.Litecoin || ctx.blockchain == Blockchain.BitcoinCash ||
ctx.blockchain == Blockchain.Stellar || ctx.blockchain == Blockchain.StellarTestNet || ctx.blockchain == Blockchain.StellarAsset) {
ctx.coinData.setIsBalanceEqual(true)

File diff suppressed because one or more lines are too long

View file

@ -54,6 +54,10 @@ public abstract class CoinData {
rate = B.getFloat("rate");
if (B.containsKey("rateAlter"))
rateAlter = B.getFloat("rateAlter");
if (B.containsKey("sentTransactionsCount")) {
sentTransactionsCount = B.getInt("sentTransactionsCount");
}
}
public void saveToBundle(Bundle B) {
@ -70,6 +74,8 @@ public abstract class CoinData {
B.putBoolean("balanceReceived", balanceReceived);
B.putString("validationNodeDescription", validationNodeDescription);
B.putInt("sentTransactionsCount", sentTransactionsCount);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
@ -83,8 +89,8 @@ public abstract class CoinData {
}
public static CoinData fromBundle(Blockchain blockchain, Bundle bundle) {
CoinEngine engine= CoinEngineFactory.INSTANCE.create(blockchain);
if( engine==null ) return null;
CoinEngine engine = CoinEngineFactory.INSTANCE.create(blockchain);
if (engine == null) return null;
CoinData result = engine.createCoinData();
result.loadFromBundle(bundle);
return result;
@ -143,11 +149,12 @@ public abstract class CoinData {
setIsBalanceEqual(false);
setBalanceReceived(false);
setValidationNodeDescription("");
minFee=null;
maxFee=null;
normalFee=null;
rate=0f;
rateAlter=0f;
minFee = null;
maxFee = null;
normalFee = null;
rate = 0f;
rateAlter = 0f;
sentTransactionsCount = 0;
}
// private AtomicInteger failedBalanceRequestCounter;
@ -191,4 +198,14 @@ public abstract class CoinData {
public CoinEngine.Amount minFee = null;
public CoinEngine.Amount normalFee = null;
public CoinEngine.Amount maxFee = null;
private int sentTransactionsCount = 0;
public int getSentTransactionsCount() {
return sentTransactionsCount;
}
public void incSentTransactionsCount() {
sentTransactionsCount++;
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.data.Blockchain
import com.tangem.wallet.bch.BtcCashEngine
import com.tangem.wallet.binance.BinanceEngine
import com.tangem.wallet.btc.BtcEngine
import com.tangem.wallet.btcmultisig.BtcMultisigEngine
import com.tangem.wallet.cardano.CardanoData
import com.tangem.wallet.cardano.CardanoEngine
import com.tangem.wallet.ducatus.DucatusEngine
@ -56,6 +57,7 @@ object CoinEngineFactory {
Blockchain.Eos -> EosEngine()
Blockchain.Ducatus -> DucatusEngine()
Blockchain.Tezos -> TezosEngine()
Blockchain.BitcoinDual -> BtcMultisigEngine()
else -> null
}
}
@ -101,6 +103,8 @@ object CoinEngineFactory {
DucatusEngine(context)
else if (Blockchain.Tezos == context.blockchain)
TezosEngine(context)
else if (Blockchain.BitcoinDual == context.blockchain)
BtcMultisigEngine(context)
else
return null
} catch (e: Exception) {

View file

@ -13,6 +13,7 @@ import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiCommon;
import com.tangem.data.network.model.BlockchainInfoAddress;
import com.tangem.data.network.model.BlockchainInfoAddressAndUnspents;
import com.tangem.data.network.model.BlockchainInfoInput;
import com.tangem.data.network.model.BlockchainInfoTransaction;
import com.tangem.data.network.model.BlockchainInfoUnspents;
import com.tangem.data.network.model.BlockchainInfoUtxo;
@ -53,6 +54,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
import okhttp3.ResponseBody;
@ -179,7 +181,7 @@ public class BtcEngine extends CoinEngine {
return false;
}
if (ctx.getBlockchain() != Blockchain.BitcoinTestNet && ctx.getBlockchain() != Blockchain.Bitcoin) {
if (ctx.getBlockchain() != Blockchain.BitcoinTestNet && ctx.getBlockchain() != Blockchain.Bitcoin && ctx.getBlockchain() != Blockchain.BitcoinDual) {
return false;
}
@ -188,7 +190,7 @@ public class BtcEngine extends CoinEngine {
}
} else {
try {
if (ctx.getBlockchain() == Blockchain.Bitcoin) {
if (ctx.getBlockchain() == Blockchain.Bitcoin || ctx.getBlockchain() == Blockchain.BitcoinDual) {
SegwitAddress.fromBech32(new MainNetParams(), address);
} else if (ctx.getBlockchain() == Blockchain.BitcoinTestNet) {
SegwitAddress.fromBech32(new TestNet3Params(), address);
@ -211,7 +213,7 @@ public class BtcEngine extends CoinEngine {
@Override
public Uri getWalletExplorerUri() {
return Uri.parse((ctx.getBlockchain() == Blockchain.Bitcoin ? "https://www.blockchain.com/btc/address/" : "https://live.blockcypher.com/btc-testnet/address/") + ctx.getCoinData().getWallet());
return Uri.parse((ctx.getBlockchain() == Blockchain.Bitcoin || ctx.getBlockchain() == Blockchain.BitcoinDual ? "https://www.blockchain.com/btc/address/" : "https://live.blockcypher.com/btc-testnet/address/") + ctx.getCoinData().getWallet());
}
@Override
@ -576,30 +578,6 @@ public class BtcEngine extends CoinEngine {
BlockchainInfoAddress blockchainInfoAddress = blockchainInfoAddressAndUnspents.getAddress();
BlockchainInfoUnspents blockchainInfoUnspents = blockchainInfoAddressAndUnspents.getUnspents();
if (blockchainInfoAddress.getFinal_balance() != null) {
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(blockchainInfoAddress.getFinal_balance());
coinData.setBalanceUnconfirmed(0L);
coinData.setValidationNodeDescription(Server.ApiBlockchainInfo.URL_BLOCKCHAININFO);
for (BlockchainInfoTransaction tx : blockchainInfoAddress.getTxs()) {
if (tx.getBlock_height() == null) {
coinData.setHasUnconfirmed(true);
}
}
if (App.pendingTransactionsStorage.hasTransactions(ctx.getCard())) {
for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) {
String pendingTxId = BTCUtils.toHex(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(pendingTx.getTx()))));
for (BlockchainInfoTransaction responseTx : blockchainInfoAddress.getTxs()) {
if (responseTx.getHash().equals(pendingTxId)) {
App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), pendingTx.getTx());
}
}
}
}
}
for (BlockchainInfoUtxo utxo : blockchainInfoUnspents.getUnspent_outputs()) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTx_hash_big_endian();
@ -609,7 +587,18 @@ public class BtcEngine extends CoinEngine {
coinData.getUnspentTransactions().add(trUnspent);
}
blockchainRequestsCallbacks.onComplete(true);
if (blockchainInfoAddress.getFinal_balance() != null) {
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(blockchainInfoAddress.getFinal_balance());
coinData.setBalanceUnconfirmed(0L);
coinData.setValidationNodeDescription(Server.ApiBlockchainInfo.URL_BLOCKCHAININFO);
}
if (blockchainInfoAddress.getTxs() != null) {
checkTransactionsBlockchainInfo(Single.just(blockchainInfoAddress.getTxs()), serverApiBlockchainInfo, blockchainRequestsCallbacks);
} else {
blockchainRequestsCallbacks.onComplete(true);
}
}
@Override
@ -648,14 +637,29 @@ public class BtcEngine extends CoinEngine {
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()) {
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);
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();
}
}
}
if (blockcypherResponse.getUnconfirmed_txrefs() != null) {
for (BlockcypherTxref unconfirmedTxref : blockcypherResponse.getUnconfirmed_txrefs()) {
if (unconfirmedTxref.getTx_input_n() != -1) {
coinData.incSentTransactionsCount();
}
}
}
} catch (Exception e) {
@ -663,7 +667,7 @@ public class BtcEngine extends CoinEngine {
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
checkPending(blockchainRequestsCallbacks);
checkPendingBlockcypher(blockchainRequestsCallbacks);
}
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
@ -686,7 +690,59 @@ public class BtcEngine extends CoinEngine {
}
}
private void checkPending(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
private void checkTransactionsBlockchainInfo(Single<List<BlockchainInfoTransaction>> txsSingle, ServerApiBlockchainInfo serverApiBlockchainInfo, BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
SingleObserver<List<BlockchainInfoTransaction>> txsObserver = new DisposableSingleObserver<List<BlockchainInfoTransaction>>() {
@Override
public void onSuccess(List<BlockchainInfoTransaction> txs) {
if (App.pendingTransactionsStorage.hasTransactions(ctx.getCard())) {
for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) {
String pendingTxId = BTCUtils.toHex(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(pendingTx.getTx()))));
for (BlockchainInfoTransaction responseTx : txs) {
if (pendingTxId.equals(responseTx.getHash())) {
App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), pendingTx.getTx());
}
}
}
}
for (BlockchainInfoTransaction tx : txs) {
if (tx.getBlock_height() == null) {
coinData.setHasUnconfirmed(true);
}
for (BlockchainInfoInput input : tx.getInputs()) {
String inputAddress = input.getPrev_out().getAddr();
if (coinData.getWallet().equals(inputAddress)) {
coinData.incSentTransactionsCount();
}
}
}
if (txs.size() == 50) {
final ServerApiBlockchainInfo serverApiBlockchainInfo = new ServerApiBlockchainInfo();
checkTransactionsBlockchainInfo(serverApiBlockchainInfo.getMoreAddressTxs(coinData.getWallet()), serverApiBlockchainInfo, blockchainRequestsCallbacks);
} else {
blockchainRequestsCallbacks.onComplete(true);
}
}
@Override
public void onError(Throwable e) {
Log.i(TAG, "onError: getMoreAddressTxs" + e.getMessage());
coinData.setUseBlockcypher(true);
try {
requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks);
} catch (Exception ex) {
ctx.setError(ex.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
}
};
txsSingle.subscribe(txsObserver);
}
private void checkPendingBlockcypher(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
if (App.pendingTransactionsStorage.hasTransactions(ctx.getCard())) {
ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();

View file

@ -0,0 +1,137 @@
package com.tangem.wallet.btcmultisig
import com.google.common.primitives.UnsignedBytes
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol.TangemException
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.wallet.TangemContext
import com.tangem.wallet.btc.BtcEngine
import org.bitcoinj.core.*
import org.bitcoinj.crypto.TransactionSignature
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.script.Script
import org.bitcoinj.script.ScriptBuilder
import java.math.BigInteger
class BtcMultisigEngine : BtcEngine {
val networkParameters = MainNetParams.get()
constructor(): super()
constructor(context: TangemContext): super(context)
override fun defineWallet() {
try {
val wallet = calculateAddress(ctx.card.walletPublicKeyRar)
ctx.coinData.wallet = wallet
} catch (e: Exception) {
ctx.coinData.wallet = "ERROR"
throw TangemException("Can't define wallet address")
}
}
override fun calculateAddress(compressedPublicKey: ByteArray): String? {
if (ctx.card.issuerData != null && ctx.card.issuerData.size == 33) {
val script = createMultisigOutputScript()
val scriptHash = Utils.sha256hash160(script.program)
val address = LegacyAddress.fromScriptHash(networkParameters, scriptHash)
return address.toBase58()
} else {
return Util.byteArrayToHexString(compressedPublicKey)
}
}
override fun constructTransaction(amountValue: Amount, feeValue: Amount, IncFee: Boolean, targetAddress: String): SignTask.TransactionToSign {
val fee = convertToInternalAmount(feeValue).longValueExact()
var amount = convertToInternalAmount(amountValue).longValueExact()
var change = coinData.balanceInInternalUnits.longValueExact() - amount
if (IncFee) {
amount -= fee
} else {
change -= fee
}
val transaction = Transaction(networkParameters)
for (utxo in coinData.unspentTransactions) {
transaction.addInput(
Sha256Hash.wrap(utxo.txID),
utxo.outputN.toLong(),
Script(Util.hexToBytes(utxo.script))
)
}
transaction.addOutput(
Coin.valueOf(amount),
Address.fromString(networkParameters, targetAddress)
)
if (change != 0L) {
transaction.addOutput(
Coin.valueOf(change),
Address.fromString(networkParameters, coinData.wallet)
)
}
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash
}
override fun getHashesToSign(): Array<ByteArray> {
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }
for (input in transaction.inputs) {
val index = input.index
val outputScript = createMultisigOutputScript()
hashesForSign[index] = transaction.hashForSignature(index, outputScript, Transaction.SigHash.ALL, false).bytes
}
return hashesForSign.toTypedArray()
}
@Throws(java.lang.Exception::class)
override fun getRawDataToSign(): ByteArray {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
override fun getHashAlgToSign(): String {
return "sha-256x2"
}
@Throws(java.lang.Exception::class)
override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray {
throw java.lang.Exception("Issuer validation not supported!")
}
override fun onSignCompleted(signFromCard: ByteArray): ByteArray {
for (index in transaction.inputs.indices) {
transaction.inputs[index].scriptSig =
createMultisigInputScript(index, signFromCard)
}
val txForSend = transaction.bitcoinSerialize()
notifyOnNeedSendTransaction(txForSend)
return txForSend
}
}
}
private fun createMultisigInputScript(index: Int, signedTransaction: ByteArray): Script {
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64))
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64))
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
val signature = TransactionSignature(r, canonicalS)
val outputScript = createMultisigOutputScript()
return ScriptBuilder.createP2SHMultiSigInputScript(mutableListOf(signature), outputScript)
}
private fun createMultisigOutputScript(): Script {
val publicKeys = mutableListOf(ctx.card.walletPublicKeyRar, ctx.card.issuerData)
publicKeys.sortWith(UnsignedBytes.lexicographicalComparator())
val publicEcKeys =
MutableList(publicKeys.size) { i -> ECKey.fromPublicOnly(publicKeys[i]) }
return Script(Script.createMultiSigOutputScript(1, publicEcKeys))
}
}

View file

@ -7,7 +7,11 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.Constant;
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.BlockcypherTxref;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.tasks.SignTask;
@ -463,7 +467,8 @@ public class EthEngine extends CoinEngine {
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiInfura serverApiInfura = new ServerApiInfura();
// request requestData listener
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
@ -501,7 +506,7 @@ public class EthEngine extends CoinEngine {
break;
}
if (serverApiInfura.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -512,7 +517,7 @@ public class EthEngine extends CoinEngine {
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
if (serverApiInfura.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(false);
} else {
blockchainRequestsCallbacks.onProgress();
@ -521,9 +526,55 @@ public class EthEngine extends CoinEngine {
};
serverApiInfura.setResponseListener(responseListener);
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()) {
if (txref.getTx_input_n() != -1) { //sent only
coinData.incSentTransactionsCount();
}
}
}
if (blockcypherResponse.getUnconfirmed_txrefs() != null) {
for (BlockcypherTxref unconfirmedTxref : blockcypherResponse.getUnconfirmed_txrefs()) {
if (unconfirmedTxref.getTx_input_n() != -1) {
coinData.incSentTransactionsCount();
}
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
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.setResponseListener(blockcypherListener);
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_BALANCE, 67, coinData.getWallet(), "", "");
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_TRANSACTION_COUNT, 67, coinData.getWallet(), "", "");
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GET_PENDING_COUNT, 67, coinData.getWallet(), "", "");
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
}
@Override

View file

@ -8,7 +8,11 @@ import android.util.Log;
import com.google.common.base.Strings;
import com.tangem.App;
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.BlockcypherTxref;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.tasks.SignTask;
@ -542,7 +546,7 @@ public class TokenEngine extends CoinEngine {
int gasLimitInt = 60000;
if (amountValue.getCurrency().equals("DGX")) {
if (amountValue.getCurrency().equals("DGX") || amountValue.getCurrency().equals("CGT")) {
gasLimitInt = 300000;
}
@ -639,7 +643,8 @@ public class TokenEngine extends CoinEngine {
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiInfura serverApiInfura = new ServerApiInfura();
// request requestData listener
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
@ -701,7 +706,7 @@ public class TokenEngine extends CoinEngine {
break;
}
if (serverApiInfura.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -712,7 +717,7 @@ public class TokenEngine extends CoinEngine {
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
if (serverApiInfura.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(false);
} else {
blockchainRequestsCallbacks.onProgress();
@ -721,12 +726,58 @@ public class TokenEngine extends CoinEngine {
};
serverApiInfura.setResponseListener(responseListener);
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()) {
if (txref.getTx_input_n() != -1) { //sent only
coinData.incSentTransactionsCount();
}
}
}
if (blockcypherResponse.getUnconfirmed_txrefs() != null) {
for (BlockcypherTxref unconfirmedTxref : blockcypherResponse.getUnconfirmed_txrefs()) {
if (unconfirmedTxref.getTx_input_n() != -1) {
coinData.incSentTransactionsCount();
}
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
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.setResponseListener(blockcypherListener);
if (validateAddress(getContractAddress(ctx.getCard()))) {
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_CALL, 67, coinData.getWallet(), getContractAddress(ctx.getCard()), "");
} else {
ctx.setError("Smart contract address not defined");
blockchainRequestsCallbacks.onComplete(false);
}
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_ADDRESS, ctx.getCoinData().getWallet(), "");
}
@Override
@ -745,7 +796,7 @@ public class TokenEngine extends CoinEngine {
Log.i(TAG, "Infura gas price: " + gasPrice + " (" + l.toString() + ")");
BigInteger m;
if (!amount.getCurrency().equals(Blockchain.Ethereum.getCurrency()))
if (amount.getCurrency().equals("DGX")) {
if (amount.getCurrency().equals("DGX") || amount.getCurrency().equals("CGT")) {
m = BigInteger.valueOf(300000);
} else {
m = BigInteger.valueOf(60000);

View file

@ -30,7 +30,7 @@ public class XlmAssetData extends CoinData {
private Long sequenceNumber = 0L;
private CoinEngine.Amount baseReserve = new CoinEngine.Amount("0.5", "XLM");
private CoinEngine.Amount baseFee = new CoinEngine.Amount("0.00001", "XLM");
private boolean error404 = false;
private boolean error404, targetAccountCreated = false;
@Override
public void clearInfo() {
@ -38,6 +38,7 @@ public class XlmAssetData extends CoinData {
xlmBalance = null;
assetBalance = null;
error404 = false;
targetAccountCreated = false;
}
CoinEngine.Amount getXlmBalance() {
@ -101,6 +102,14 @@ public class XlmAssetData extends CoinData {
return true;
}
public boolean isTargetAccountCreated() {
return targetAccountCreated;
}
public void setTargetAccountCreated(boolean targetAccountCreated) {
this.targetAccountCreated = targetAccountCreated;
}
@Override
public void loadFromBundle(Bundle B) {
super.loadFromBundle(B);
@ -137,6 +146,10 @@ public class XlmAssetData extends CoinData {
if (B.containsKey("Error404")) error404 = B.getBoolean("Error404");
else error404 = false;
if (B.containsKey("TargetAccountCreated"))
targetAccountCreated = B.getBoolean("TargetAccountCreated");
else targetAccountCreated = false;
}
@Override
@ -169,6 +182,8 @@ public class XlmAssetData extends CoinData {
if (error404) B.putBoolean("Error404", true);
if (targetAccountCreated) B.putBoolean("TargetAccountCreated", true);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}

View file

@ -1,12 +1,10 @@
package com.tangem.wallet.xlm;
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.tangem_card.data.TangemCard;
@ -28,6 +26,7 @@ import org.stellar.sdk.Operation;
import org.stellar.sdk.PaymentOperation;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionEx;
import org.stellar.sdk.responses.operations.OperationResponse;
import java.io.IOException;
import java.math.BigDecimal;
@ -90,7 +89,7 @@ public class XlmAssetEngine extends CoinEngine {
if (balance != null) {
if (!coinData.isAssetBalanceZero()) {
return " " + assetBalance.toDescriptionString(getDecimals()) + "<br><small><small>"+ balance.toDescriptionString(getDecimals()) + " for fee + " + coinData.getReserve().toDescriptionString(getDecimals()) + " reserve</small></small>";
return " " + assetBalance.toDescriptionString(getDecimals()) + "<br><small><small>" + balance.toDescriptionString(getDecimals()) + " for fee + " + coinData.getReserve().toDescriptionString(getDecimals()) + " reserve</small></small>";
}
return " " + balance.toDescriptionString(getDecimals()) + "<br><small><small>+ " + coinData.getReserve().toDescriptionString(getDecimals()) + " reserve</small></small>";
} else {
@ -171,7 +170,7 @@ public class XlmAssetEngine extends CoinEngine {
// if (ctx.getCard().getDenomination() != null) {
// return Uri.parse(ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString());
// } else {
return Uri.parse(ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
// }
}
@ -186,7 +185,7 @@ public class XlmAssetEngine extends CoinEngine {
Amount balance;
if (!coinData.isAssetBalanceZero()) {
balance = coinData.getAssetBalance();
balance = coinData.getAssetBalance();
} else {
balance = coinData.getXlmBalance();
}
@ -377,9 +376,6 @@ public class XlmAssetEngine extends CoinEngine {
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
if (coinData.isAssetBalanceZero() && IncFee) {
amountValue = new Amount(amountValue.subtract(feeValue), amountValue.getCurrency());
@ -392,13 +388,13 @@ public class XlmAssetEngine extends CoinEngine {
if (!coinData.isAssetBalanceZero()) {
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), Asset.createNonNativeAsset(ctx.getCard().getTokenSymbol(), KeyPair.fromAccountId(ctx.getCard().getContractAddress())), amountValue.toValueString()).build();
} else {
if (isAccountCreated(targetAddress))
if (coinData.isTargetAccountCreated())
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), new AssetTypeNative(), amountValue.toValueString()).build();
else
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build();
}
}
TransactionEx transaction = TransactionEx.buildEx(60, coinData.getAccountResponse(), operation);
TransactionEx transaction = TransactionEx.buildEx(120, coinData.getAccountResponse(), operation);
if (transaction.getFee() != convertToInternalAmount(feeValue).intValueExact()) {
@ -447,23 +443,40 @@ public class XlmAssetEngine extends CoinEngine {
}
// network call inside, don't use on main thread
private boolean isAccountCreated(String address) {
private void checkTargetAccountCreated(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
final ServerApiStellar serverApi = new ServerApiStellar(ctx.getBlockchain());
StellarRequest.Balance request = new StellarRequest.Balance(address);
ServerApiStellar.Listener listener = new ServerApiStellar.Listener() {
@Override
public void onSuccess(StellarRequest.Base request) {
coinData.setTargetAccountCreated(true);
blockchainRequestsCallbacks.onComplete(true);
}
try {
serverApi.doStellarRequest(ctx, request);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
return true; // suppose account is created if anything goes wrong TODO:check
}
if (request.errorResponse != null && request.errorResponse.getCode() == 404)
return false;
else
return true;
@Override
public void onFail(StellarRequest.Base request) {
Log.i(TAG, "onFail: " + request.getClass().getSimpleName() + " " + request.getError());
if (request.errorResponse != null && request.errorResponse.getCode() == 404) {
coinData.setTargetAccountCreated(false);
if (amount.compareTo(coinData.getReserve()) >= 0) { //TODO: take fee inclusion in account, now 1 XLM with fee included will fail after transaction is sent
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.setTargetAccountCreated(true);
blockchainRequestsCallbacks.onComplete(true);
}
}
};
serverApi.setListener(listener);
serverApi.requestData(ctx, new StellarRequest.Balance(targetAddress));
}
@Override
@ -496,6 +509,21 @@ public class XlmAssetEngine extends CoinEngine {
} else {
blockchainRequestsCallbacks.onProgress();
}
} else if (request instanceof StellarRequest.Operations) {
StellarRequest.Operations operationsRequest = (StellarRequest.Operations) request;
for (OperationResponse operationResponse : operationsRequest.operationsList) {
if (operationResponse.getSourceAccount().getAccountId().equals(coinData.getWallet())) {
coinData.incSentTransactionsCount();
}
}
if (serverApi.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
} else {
ctx.setError("Invalid request logic");
blockchainRequestsCallbacks.onComplete(false);
@ -529,13 +557,14 @@ public class XlmAssetEngine extends CoinEngine {
serverApi.requestData(ctx, new StellarRequest.Balance(coinData.getWallet()));
serverApi.requestData(ctx, new StellarRequest.Ledgers());
serverApi.requestData(ctx, new StellarRequest.Operations(coinData.getWallet()));
}
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
// TODO: get fee stats?
coinData.minFee = coinData.normalFee = coinData.maxFee = coinData.getBaseFee();
blockchainRequestsCallbacks.onComplete(true);
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount);
}
@Override

View file

@ -26,6 +26,7 @@ import org.stellar.sdk.Operation;
import org.stellar.sdk.PaymentOperation;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionEx;
import org.stellar.sdk.responses.operations.OperationResponse;
import java.io.IOException;
import java.math.BigDecimal;
@ -97,7 +98,7 @@ public class XlmEngine extends CoinEngine {
@Override
public boolean hasBalanceInfo() {
if (coinData == null) return false;
return (coinData.getBalance() != null) || (coinData.isError404()) ;
return (coinData.getBalance() != null) || (coinData.isError404());
}
@ -365,7 +366,7 @@ public class XlmEngine extends CoinEngine {
else
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build();
TransactionEx transaction = TransactionEx.buildEx(60, coinData.getAccountResponse(), operation);
TransactionEx transaction = TransactionEx.buildEx(120, coinData.getAccountResponse(), operation);
if (transaction.getFee() != convertToInternalAmount(feeValue).intValueExact()) {
@ -470,6 +471,7 @@ public class XlmEngine extends CoinEngine {
} else {
blockchainRequestsCallbacks.onProgress();
}
} else if (request instanceof StellarRequest.Ledgers) {
StellarRequest.Ledgers ledgersRequest = (StellarRequest.Ledgers) request;
@ -480,6 +482,22 @@ public class XlmEngine extends CoinEngine {
} else {
blockchainRequestsCallbacks.onProgress();
}
} else if (request instanceof StellarRequest.Operations) {
StellarRequest.Operations operationsRequest = (StellarRequest.Operations) request;
for (OperationResponse operationResponse : operationsRequest.operationsList) {
if (operationResponse.getSourceAccount().getAccountId().equals(coinData.getWallet())) {
coinData.incSentTransactionsCount();
}
}
if (serverApi.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
} else {
ctx.setError("Invalid request logic");
blockchainRequestsCallbacks.onComplete(false);
@ -513,6 +531,7 @@ public class XlmEngine extends CoinEngine {
serverApi.requestData(ctx, new StellarRequest.Balance(coinData.getWallet()));
serverApi.requestData(ctx, new StellarRequest.Ledgers());
serverApi.requestData(ctx, new StellarRequest.Operations(coinData.getWallet()));
}
@Override

1
blockchain/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

78
blockchain/build.gradle Normal file
View file

@ -0,0 +1,78 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.9.2'
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2")
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.3"
implementation 'org.bitcoinj:bitcoinj-core:0.15.2'
implementation 'com.github.stellar:java-stellar-sdk:0.13.0'
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.0"
ext.kethereum_version = '0.79.5'
implementation "com.github.walleth.kethereum:functions:$kethereum_version"
implementation "com.github.walleth.kethereum:keccak_shortcut:$kethereum_version"
implementation "com.github.walleth.kethereum:wallet:$kethereum_version"
implementation "com.github.walleth.kethereum:crypto_impl_spongycastle:$kethereum_version"
implementation "com.github.walleth.kethereum:crypto:$kethereum_version"
implementation "com.github.walleth.kethereum:crypto_api:$kethereum_version"
implementation "com.github.walleth.kethereum:model:$kethereum_version"
implementation 'com.github.komputing.khex:core:1.0.0-RC6'
implementation 'com.github.komputing.khex:extensions:1.0.0-RC6'
implementation 'co.nstant.in:cbor:0.8'
implementation files('libs/ripple-core-0.0.1.jar')
//4 dependencies for ripple-core
implementation 'net.i2p.crypto:eddsa:0.3.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.61'
//noinspection DuplicatePlatformClasses
implementation 'org.json:json:20180813'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

Binary file not shown.

21
blockchain/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1 @@
<manifest package="com.tangem.blockchain" />

View file

@ -0,0 +1,72 @@
package com.tangem.blockchain.bitcoin
import com.tangem.common.extensions.calculateRipemd160
import com.tangem.common.extensions.calculateSha256
import org.bitcoinj.core.AddressFormatException
import org.bitcoinj.core.Base58
import org.bitcoinj.core.SegwitAddress
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import java.security.MessageDigest
class BitcoinAddressFactory {
companion object {
fun makeAddress(walletPublicKey: ByteArray, testNet: Boolean = false): String {
val netSelectionByte = if (testNet) 0x6f.toByte() else 0x00.toByte()
val hash1 = walletPublicKey.calculateSha256().calculateRipemd160()
val hash2 = byteArrayOf(netSelectionByte).plus(hash1).calculateSha256().calculateSha256()
val result = byteArrayOf(netSelectionByte) + hash1 + hash2[0] + hash2[1] + hash2[2] + hash2[3]
return Base58.encode(result)
}
}
}
class BitcoinAddressValidator {
companion object {
private const val firstLetters = "123nm"
private const val firstLettersNonTestNet = "13"
fun validate(address: String, testNet: Boolean = false): Boolean {
if (firstLetters.contains(address.first())) {
if (testNet && firstLettersNonTestNet.contains(address.first())) return false
if (address.length !in 26..35) return false
val decoded = address.decodeBase58() ?: return false
val hash = recursiveSha256(decoded, 0, 21, 2)
return hash.sliceArray(0..3).contentEquals(decoded.sliceArray(21..24))
} else {
return validateSegwitAddress(address, testNet)
}
}
private fun recursiveSha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray {
if (recursion == 0) return data
val md = MessageDigest.getInstance("SHA-256")
md.update(data.sliceArray(start until start + len))
return recursiveSha256(md.digest(), 0, 32, recursion - 1)
}
private fun String.decodeBase58(): ByteArray? {
return try {
Base58.decode(this)
} catch (exception: AddressFormatException) {
null
}
}
private fun validateSegwitAddress(address: String, testNet: Boolean): Boolean {
return try {
if (testNet) {
SegwitAddress.fromBech32(TestNet3Params(), address)
true
} else {
SegwitAddress.fromBech32(MainNetParams(), address)
true
}
} catch (e: Exception) {
false
}
}
}
}

View file

@ -0,0 +1,99 @@
package com.tangem.blockchain.bitcoin
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.toCanonicalised
import org.bitcoinj.core.*
import org.bitcoinj.crypto.TransactionSignature
import org.bitcoinj.script.Script
import org.bitcoinj.script.ScriptBuilder
import java.math.BigDecimal
import java.math.BigInteger
class BitcoinTransactionBuilder(private val testNet: Boolean) {
private lateinit var transaction: Transaction
private var networkParameters: NetworkParameters? = null
var unspentOutputs: List<UnspentTransaction>? = null
fun buildToSign(
transactionData: TransactionData): Result<List<ByteArray>> {
if (unspentOutputs == null) return Result.Failure(Exception("Currently there's an unconfirmed transaction"))
val change: BigDecimal = calculateChange(transactionData)
networkParameters = if (testNet) {
NetworkParameters.fromID(NetworkParameters.ID_TESTNET)
} else {
NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
}
transaction = transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs!!, change)
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }
for (input in transaction.inputs) {
val index = input.index
hashesForSign[index] = transaction.hashForSignature(index, input.scriptBytes, Transaction.SigHash.ALL, false).bytes
}
return Result.Success(hashesForSign)
}
private fun calculateChange(transactionData: TransactionData): BigDecimal {
val fullAmount = unspentOutputs!!.map { it.amount }.reduce { acc, number -> acc + number }
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.value
?: 0.toBigDecimal()))
}
fun buildToSend(signedTransaction: ByteArray, publicKey: ByteArray): ByteArray {
for (index in transaction.inputs.indices) {
transaction.inputs[index].scriptSig = createScript(index, signedTransaction, publicKey)
}
return transaction.bitcoinSerialize()
}
private fun createScript(index: Int, signedTransaction: ByteArray, publicKey: ByteArray): Script {
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64))
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64))
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
val signature = TransactionSignature(r, canonicalS)
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
}
fun getEstimateSize(transactionData: TransactionData, walletPublicKey: ByteArray): Result<Int> {
val buildTransactionResult = buildToSign(transactionData)
when (buildTransactionResult) {
is Result.Failure -> return buildTransactionResult
is Result.Success -> {
val hashes = buildTransactionResult.data
val finalTransaction = buildToSend(ByteArray(64 * hashes.size) { 1 }, walletPublicKey)
return Result.Success(finalTransaction.size)
}
}
}
}
internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkParameters?,
unspentOutputs: List<UnspentTransaction>,
change: BigDecimal): Transaction {
val transaction = Transaction(networkParameters)
for (utxo in unspentOutputs) {
transaction.addInput(Sha256Hash.wrap(utxo.hash), utxo.outputIndex, Script(utxo.outputScript))
}
transaction.addOutput(
Coin.parseCoin(this.amount.value!!.toPlainString()),
Address.fromString(networkParameters, this.destinationAddress))
if (change != 0.toBigDecimal()) {
transaction.addOutput(
Coin.parseCoin(change.toPlainString()),
Address.fromString(networkParameters,
this.sourceAddress))
}
return transaction
}
class UnspentTransaction(
val amount: BigDecimal,
val outputIndex: Long,
val hash: ByteArray,
val outputScript: ByteArray
)

View file

@ -0,0 +1,109 @@
package com.tangem.blockchain.bitcoin
import android.util.Log
import com.tangem.blockchain.bitcoin.network.BitcoinAddressResponse
import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager
import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager.Companion.SATOSHI_IN_BTC
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.common.extensions.toHexString
import com.tangem.tasks.TaskEvent
import java.math.BigDecimal
class BitcoinWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig,
isTestNet: Boolean = false
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = if (isTestNet) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = BitcoinTransactionBuilder(isTestNet)
private val networkManager = BitcoinNetworkManager(isTestNet)
override suspend fun update() {
val response = networkManager.getInfo(address)
when (response) {
is Result.Success -> updateWallet(response.data)
is Result.Failure -> updateError(response.error)
}
}
private fun updateWallet(response: BitcoinAddressResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
currencyWallet.balances[AmountType.Coin]?.value = response.balance
transactionBuilder.unspentOutputs = response.unspentTransactions
if (response.hasUnconfirmed) {
if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
}
} else {
currencyWallet.pendingTransactions.clear()
}
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val buildTransactionResult = transactionBuilder.buildToSign(transactionData)
when (buildTransactionResult) {
is Result.Failure -> return SimpleResult.Failure(buildTransactionResult.error)
is Result.Success -> {
when (val signerResponse = signer.sign(buildTransactionResult.data.toTypedArray(), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature, walletPublicKey)
return networkManager.sendTransaction(transactionToSend.toHexString())
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
when (val result = networkManager.getFee()) {
is Result.Failure -> return result
is Result.Success -> {
val sizeResult = transactionBuilder.getEstimateSize(
TransactionData(amount,
Amount(1.toBigDecimal().divide(SATOSHI_IN_BTC), blockchain),
address, destination),
walletPublicKey
)
when (sizeResult) {
is Result.Failure -> return sizeResult
is Result.Success -> {
val transactionSize = sizeResult.data.toBigDecimal()
val minFee = result.data.minimalPerKb.calculateFee(transactionSize)
val normalFee = result.data.normalPerKb.calculateFee(transactionSize)
val priorityFee = result.data.priorityPerKb.calculateFee(transactionSize)
return Result.Success(
listOf(Amount(minFee, blockchain),
Amount(normalFee, blockchain),
Amount(priorityFee, blockchain))
)
}
}
}
}
}
private fun BigDecimal.calculateFee(transactionSize: BigDecimal): BigDecimal {
val bytesInKb = BigDecimal(1024)
return this.divide(bytesInKb).multiply(transactionSize)
.setScale(8, blockchain.roundingMode())
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.blockchain.bitcoin.network
import com.tangem.blockchain.bitcoin.UnspentTransaction
import com.tangem.blockchain.bitcoin.network.api.BlockchainInfoApi
import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi
import com.tangem.blockchain.bitcoin.network.api.EstimatefeeApi
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_BLOCKCHAIN_INFO
import com.tangem.blockchain.common.network.API_BLOCKCYPHER
import com.tangem.blockchain.common.network.API_ESTIMATEFEE
import com.tangem.blockchain.common.network.createRetrofitInstance
import retrofit2.HttpException
import java.io.IOException
import java.math.BigDecimal
class BitcoinNetworkManager(private val isTestNet: Boolean) : BitcoinProvider {
private val blockcypherProvider by lazy {
val api = createRetrofitInstance(API_BLOCKCYPHER)
.create(BlockcypherApi::class.java)
BlockcypherProvider(api, isTestNet)
}
private val blockchainInfoProvider by lazy {
val api = createRetrofitInstance(API_BLOCKCHAIN_INFO)
.create(BlockchainInfoApi::class.java)
val estimateFeeApi = createRetrofitInstance(API_ESTIMATEFEE)
.create(EstimatefeeApi::class.java)
BlockchainInfoProvider(api, estimateFeeApi)
}
private var bitcoinProvider: BitcoinProvider = blockchainInfoProvider
private fun changeProvider() {
bitcoinProvider = if (bitcoinProvider == blockchainInfoProvider) {
blockcypherProvider
} else {
blockchainInfoProvider
}
}
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
val result = bitcoinProvider.getInfo(address)
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return bitcoinProvider.getInfo(address)
} else {
return result
}
}
}
}
override suspend fun getFee(): Result<BitcoinFee> {
val result = bitcoinProvider.getFee()
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return bitcoinProvider.getFee()
} else {
return result
}
}
}
}
override suspend fun sendTransaction(transaction: String): SimpleResult {
val result = bitcoinProvider.sendTransaction(transaction)
when (result) {
is SimpleResult.Success -> return result
is SimpleResult.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return bitcoinProvider.sendTransaction(transaction)
} else {
return result
}
}
}
}
companion object {
val SATOSHI_IN_BTC = 100000000.toBigDecimal()
}
}
data class BitcoinAddressResponse(
val balance: BigDecimal,
val hasUnconfirmed: Boolean,
val unspentTransactions: List<UnspentTransaction>?
)
data class BitcoinFee(
val minimalPerKb: BigDecimal,
val normalPerKb: BigDecimal,
val priorityPerKb: BigDecimal
)

View file

@ -0,0 +1,10 @@
package com.tangem.blockchain.bitcoin.network
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
interface BitcoinProvider {
suspend fun getInfo(address: String): Result<BitcoinAddressResponse>
suspend fun getFee(): Result<BitcoinFee>
suspend fun sendTransaction(transaction: String): SimpleResult
}

View file

@ -0,0 +1,80 @@
package com.tangem.blockchain.bitcoin.network
import com.tangem.blockchain.bitcoin.UnspentTransaction
import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager.Companion.SATOSHI_IN_BTC
import com.tangem.blockchain.bitcoin.network.api.BlockchainInfoApi
import com.tangem.blockchain.bitcoin.network.api.EstimatefeeApi
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.retryIO
import com.tangem.common.extensions.hexToBytes
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class BlockchainInfoProvider(
private val blockchainApi: BlockchainInfoApi,
private val estimatefeeApi: EstimatefeeApi
) : BitcoinProvider {
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
return try {
coroutineScope {
val addressDeferred = retryIO { async { blockchainApi.getAddress(address) } }
val unspentsDeferred = retryIO { async { blockchainApi.getUnspents(address) } }
val addressData = addressDeferred.await()
val unspents = unspentsDeferred.await()
val unconfirmedTransactions = addressData.transactions?.find { it.blockHeight == 0L } != null
val bitcoinUnspents = unspents.unspentOutputs.map {
UnspentTransaction(
it.amount!!.toBigDecimal().divide(SATOSHI_IN_BTC),
it.outputIndex!!.toLong(),
it.hash!!.hexToBytes(),
it.outputScript!!.hexToBytes())
}
Result.Success(
BitcoinAddressResponse(
addressData.finalBalance?.toBigDecimal()?.divide(SATOSHI_IN_BTC)
?: 0.toBigDecimal(), unconfirmedTransactions, bitcoinUnspents))
}
} catch (exception: Exception) {
Result.Failure(exception)
}
}
override suspend fun getFee(): Result<BitcoinFee> {
return try {
coroutineScope {
val minFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeeMinimal() } }
val normalFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeeNormal() } }
val priorityFeeDeferred = retryIO { async { estimatefeeApi.getEstimateFeePriority() } }
val minFee = minFeeDeferred.await()
val normalFee = normalFeeDeferred.await()
val priorityFee = priorityFeeDeferred.await()
Result.Success(BitcoinFee(
minFee.toBigDecimal(),
normalFee.toBigDecimal(),
priorityFee.toBigDecimal()))
}
} catch (exception: Exception) {
Result.Failure(exception)
}
}
override suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
retryIO { blockchainApi.sendTransaction(transaction) }
SimpleResult.Success
} catch (exception: Exception) {
SimpleResult.Failure(exception)
}
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.blockchain.bitcoin.network
import com.tangem.blockchain.bitcoin.UnspentTransaction
import com.tangem.blockchain.bitcoin.network.BitcoinNetworkManager.Companion.SATOSHI_IN_BTC
import com.tangem.blockchain.bitcoin.network.api.BlockcypherApi
import com.tangem.blockchain.bitcoin.network.api.BlockcypherBody
import com.tangem.blockchain.bitcoin.network.response.BlockcypherFee
import com.tangem.blockchain.bitcoin.network.response.BlockcypherResponse
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.retryIO
import com.tangem.common.extensions.hexToBytes
class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : BitcoinProvider {
private val blockchain = "btc"
private val network = if (isTestNet) {
BlockcypherNetwork.Test.network
} else {
BlockcypherNetwork.Main.network
}
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
try {
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) }
val unspents = addressData.txrefs?.map {
UnspentTransaction(
it.amount!!.toBigDecimal().divide(SATOSHI_IN_BTC),
it.outputIndex!!.toLong(),
it.hash!!.hexToBytes(),
it.outputScript!!.hexToBytes()
)
}
return Result.Success(BitcoinAddressResponse(
addressData.balance!!.toBigDecimal().divide(SATOSHI_IN_BTC),
addressData.unconfirmedBalance != 0L,
unspents))
} catch (error: Exception) {
return Result.Failure(error)
}
}
override suspend fun getFee(): Result<BitcoinFee> {
try {
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) }
return Result.Success(
BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal().divide(SATOSHI_IN_BTC),
receivedFee.normalFeePerKb!!.toBigDecimal().divide(SATOSHI_IN_BTC),
receivedFee.priorityFeePerKb!!.toBigDecimal().divide(SATOSHI_IN_BTC))
)
} catch (error: Exception) {
return Result.Failure(error)
}
}
override suspend fun sendTransaction(transaction: String): SimpleResult {
try {
retryIO {
api.sendTransaction(
blockchain, network, BlockcypherBody(transaction), BlockcypherToken.getToken())
}
return SimpleResult.Success
} catch (error: Exception) {
return SimpleResult.Failure(error)
}
}
}
private object BlockcypherToken {
private val tokens = listOf(
"aa8184b0e0894b88a5688e01b3dc1e82",
"56c4ca23c6484c8f8864c32fde4def8d",
"66a8a37c5e9d4d2c9bb191acfe7f93aa")
fun getToken(): String = tokens.random()
}
private enum class BlockcypherNetwork(val network: String) {
Main("main"),
Test("test3")
}

View file

@ -0,0 +1,18 @@
package com.tangem.blockchain.bitcoin.network.api
import com.tangem.blockchain.bitcoin.network.response.BlockchainInfoAddress
import com.tangem.blockchain.bitcoin.network.response.BlockchainInfoUnspents
import okhttp3.ResponseBody
import retrofit2.http.*
interface BlockchainInfoApi {
@GET("rawaddr/{address}?limit=5")
suspend fun getAddress(@Path("address") address: String): BlockchainInfoAddress
@GET("unspent")
suspend fun getUnspents(@Query("active") address: String): BlockchainInfoUnspents
@FormUrlEncoded
@POST("pushtx")
suspend fun sendTransaction(@Field("tx") transaction: String): ResponseBody
}

View file

@ -0,0 +1,41 @@
package com.tangem.blockchain.bitcoin.network.api
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.bitcoin.network.response.BlockcypherFee
import com.tangem.blockchain.bitcoin.network.response.BlockcypherResponse
import com.tangem.blockchain.bitcoin.network.response.BlockcypherTx
import retrofit2.http.*
interface BlockcypherApi {
@GET("v1/{blockchain}/{network}")
suspend fun getFee(
@Path("blockchain") blockchain: String,
@Path("network") network: String
): BlockcypherFee
@GET("v1/{blockchain}/{network}/addrs/{address}?unspentOnly=true&includeScript=true")
suspend fun getAddressData(
@Path("blockchain") blockchain: String,
@Path("network") network: String,
@Path("address") address: String
): BlockcypherResponse
@GET("v1/{blockchain}/{network}/txs/{txHash}?includeHex=true")
suspend fun getTransactions(
@Path("blockchain") blockchain: String,
@Path("network") network: String,
@Path("txHash") txHash: String
): BlockcypherTx
@Headers("Content-Type: application/json")
@POST("v1/{blockchain}/{network}/txs/push")
suspend fun sendTransaction(
@Path("blockchain") blockchain: String,
@Path("network") network: String,
@Body blockcypherBody: BlockcypherBody,
@Query("token") token: String
): BlockcypherTx
}
@JsonClass(generateAdapter = true)
data class BlockcypherBody(val tx: String)

View file

@ -0,0 +1,16 @@
package com.tangem.blockchain.bitcoin.network.api
import retrofit2.http.GET
interface EstimatefeeApi {
@GET(ESTIMATE_FEE_URL + "n/2")
suspend fun getEstimateFeePriority(): String
@GET(ESTIMATE_FEE_URL + "n/3")
suspend fun getEstimateFeeNormal(): String
@GET(ESTIMATE_FEE_URL + "n/6")
suspend fun getEstimateFeeMinimal(): String
}
const val ESTIMATE_FEE_URL = "https://estimatefee.com/"

View file

@ -0,0 +1,43 @@
package com.tangem.blockchain.bitcoin.network.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class BlockchainInfoAddress(
@Json(name = "final_balance")
val finalBalance: Long? = null,
@Json(name = "txs")
val transactions: List<BlockchainInfoTransaction>? = null
)
@JsonClass(generateAdapter = true)
data class BlockchainInfoTransaction(
@Json(name = "hash")
val hash: String? = null,
@Json(name = "block_height")
val blockHeight: Long? = null
)
@JsonClass(generateAdapter = true)
data class BlockchainInfoUnspents(
@Json(name = "unspent_outputs")
val unspentOutputs: List<BlockchainInfoUtxo>
)
@JsonClass(generateAdapter = true)
data class BlockchainInfoUtxo(
@Json(name = "tx_hash_big_endian")
val hash: String? = null,
@Json(name = "tx_output_n")
val outputIndex: Int? = null,
@Json(name = "value")
val amount: Long? = null,
@Json(name = "script")
val outputScript: String? = null
)

View file

@ -0,0 +1,55 @@
package com.tangem.blockchain.bitcoin.network.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class BlockcypherResponse(
@Json(name = "address")
val address: String? = null,
@Json(name = "balance")
val balance: Long? = null,
@Json(name = "unconfirmed_balance")
val unconfirmedBalance: Long? = null,
@Json(name = "txrefs")
val txrefs: List<BlockcypherTxref>? = null
)
@JsonClass(generateAdapter = true)
data class BlockcypherTxref(
@Json(name = "tx_hash")
val hash: String? = null,
@Json(name = "tx_output_n")
val outputIndex: Int? = null,
@Json(name = "value")
val amount: Long? = null,
@Json(name = "confirmations")
val confirmations: Long? = null,
@Json(name = "script")
val outputScript: String? = null
)
@JsonClass(generateAdapter = true)
data class BlockcypherTx(
@Json(name = "hex")
val hex: String? = null
)
@JsonClass(generateAdapter = true)
data class BlockcypherFee(
@Json(name = "low_fee_per_kb")
val minFeePerKb: Long? = null,
@Json(name = "medium_fee_per_kb")
val normalFeePerKb: Long? = null,
@Json(name = "high_fee_per_kb")
val priorityFeePerKb: Long? = null
)

View file

@ -0,0 +1,105 @@
package com.tangem.blockchain.cardano
import co.nstant.`in`.cbor.CborBuilder
import co.nstant.`in`.cbor.CborDecoder
import co.nstant.`in`.cbor.CborEncoder
import co.nstant.`in`.cbor.model.Array
import co.nstant.`in`.cbor.model.ByteString
import co.nstant.`in`.cbor.model.UnsignedInteger
import com.tangem.blockchain.cardano.crypto.Blake2b
import com.tangem.blockchain.common.extensions.decodeBase58
import com.tangem.blockchain.common.extensions.encodeBase58
import org.spongycastle.crypto.util.DigestFactory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.CRC32
class CardanoAddressFactory {
companion object {
fun makeAddress(cardPublicKey: ByteArray, testNet: Boolean = false): String {
val extendedPublicKey = extendPublicKey(cardPublicKey)
val pubKeyWithAttributesBaos = ByteArrayOutputStream()
CborEncoder(pubKeyWithAttributesBaos).encode(CborBuilder()
.addArray()
.add(0)
.addArray()
.add(0)
.add(extendedPublicKey)
.end()
.addMap()
.end()
.end()
.build())
val pubKeyWithAttributes = pubKeyWithAttributesBaos.toByteArray()
val sha3Digest = DigestFactory.createSHA3_256()
sha3Digest.update(pubKeyWithAttributes, 0, pubKeyWithAttributes.size)
val sha3Hash = ByteArray(32)
sha3Digest.doFinal(sha3Hash, 0)
val blake2b = Blake2b.Digest.newInstance(28)
val blakeHash = blake2b.digest(sha3Hash)
val hashWithAttributesBaos = ByteArrayOutputStream()
CborEncoder(hashWithAttributesBaos).encode(CborBuilder()
.addArray()
.add(blakeHash)
.addMap() //additional attributes
.end()
.add(0) //address type
.end()
.build())
val hashWithAttributes = hashWithAttributesBaos.toByteArray()
val crc32 = CRC32()
crc32.update(hashWithAttributes)
val checksum = crc32.value
val addressItem = CborBuilder().add(hashWithAttributes).build().get(0)
addressItem.setTag(24)
//addr + checksum
val addressBaos = ByteArrayOutputStream()
CborEncoder(addressBaos).encode(CborBuilder()
.addArray()
.add(addressItem)
.add(checksum)
.end()
.build())
val hexAddress = addressBaos.toByteArray()
return hexAddress.encodeBase58()
}
fun extendPublicKey(publicKey: ByteArray): ByteArray {
val zeroBytes = ByteArray(32)
zeroBytes.fill(0)
return publicKey + zeroBytes
}
}
}
class CardanoAddressValidator {
companion object {
fun validate(address: String): Boolean {
val decoded = address.decodeBase58() ?: return false
return try {
val bais = ByteArrayInputStream(decoded)
val addressList =
(CborDecoder(bais).decode()[0] as Array).dataItems
val addressItemBytes = (addressList[0] as ByteString).bytes
val checksum = (addressList[1] as UnsignedInteger).value.toLong()
val crc32 = CRC32()
crc32.update(addressItemBytes)
val calculatedChecksum = crc32.value
checksum == calculatedChecksum
} catch (e: Exception) {
false
}
}
}
}

View file

@ -0,0 +1,171 @@
package com.tangem.blockchain.cardano
import co.nstant.`in`.cbor.CborBuilder
import co.nstant.`in`.cbor.CborDecoder
import co.nstant.`in`.cbor.CborEncoder
import co.nstant.`in`.cbor.builder.ArrayBuilder
import com.tangem.blockchain.cardano.crypto.Blake2b
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.extensions.decodeBase58
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigDecimal
class CardanoTransactionBuilder() {
var unspentOutputs: List<UnspentOutput> = listOf()
var transactionBody: ByteArray = ByteArray(0)
fun buildToSign(transactionData: TransactionData): ByteArray {
val transactionBuilder = CborBuilder()
val transactionArray = transactionBuilder.addArray()
transactionArray.addInputArray()
transactionArray.addOutputArray(transactionData)
transactionArray.addMap().end()
val transactionBaos = ByteArrayOutputStream()
CborEncoder(transactionBaos).encode(transactionBuilder.build())
transactionBody = transactionBaos.toByteArray()
val blake2b = Blake2b.Digest.newInstance(32)
val transactionBodyHash = blake2b.digest(transactionBody)
val magicBaos = ByteArrayOutputStream()
CborEncoder(magicBaos).encode(CborBuilder().add(PROTOCOL_MAGIC).build())
val magic = magicBaos.toByteArray()
//dataToSign prefix
val prefixedHashBaos = ByteArrayOutputStream()
prefixedHashBaos.write(byteArrayOf(0x01.toByte()))
prefixedHashBaos.write(magic)
prefixedHashBaos.write(byteArrayOf(0x58.toByte(), 0x20.toByte()))
prefixedHashBaos.write(transactionBodyHash)
return prefixedHashBaos.toByteArray()
}
fun buildToSend(signature: ByteArray, publicKey: ByteArray): ByteArray {
val extendedPublicKey = CardanoAddressFactory.extendPublicKey(publicKey)
//pubkey + signature
val witnessBodyBaos = ByteArrayOutputStream()
CborEncoder(witnessBodyBaos).encode(CborBuilder()
.addArray()
.add(extendedPublicKey)
.add(signature)
.end()
.build())
val witnessBody = witnessBodyBaos.toByteArray()
val witnessBodyItem = CborBuilder().add(witnessBody).build()[0]
witnessBodyItem.setTag(24)
val witnessBuilder = CborBuilder()
val witnessArrayBuilder = witnessBuilder.addArray()
//witness type + witness body
for (utxo in unspentOutputs) {
witnessArrayBuilder
.addArray()
.add(0)
.add(witnessBodyItem)
.end()
}
val witnessBaos = ByteArrayOutputStream()
CborEncoder(witnessBaos).encode(witnessBuilder.build())
val witness = witnessBaos.toByteArray()
val transactionBaos = ByteArrayOutputStream()
transactionBaos.write(byteArrayOf(0x82.toByte()))
transactionBaos.write(transactionBody)
transactionBaos.write(witness)
return transactionBaos.toByteArray()
}
private fun ArrayBuilder<CborBuilder>.addInputArray() {
val inputArray = this.startArray()
for (utxo in unspentOutputs) {
val inputBaos = ByteArrayOutputStream()
CborEncoder(inputBaos).encode(CborBuilder()
.addArray()
.add(utxo.hash)
.add(utxo.outputIndex)
.end()
.build())
val input = inputBaos.toByteArray()
val inputItem = CborBuilder().add(input).build().get(0)
inputItem.setTag(24)
//input type + input
inputArray
.addArray()
.add(0)
.add(inputItem)
.end()
}
inputArray.end()
}
private fun ArrayBuilder<CborBuilder>.addOutputArray(transactionData: TransactionData) {
val amount = transactionData.amount.value!!
.movePointRight(transactionData.amount.decimals.toInt()).toLong()
val fee = transactionData.fee!!.value!!
.movePointRight(transactionData.fee.decimals.toInt()).toLong()
val change = calculateChange(amount, fee)
val outputArray = this.startArray()
//1st output
val targetAddressItem =
CborDecoder(ByteArrayInputStream(transactionData.destinationAddress.decodeBase58()))
.decode()[0]
outputArray
.addArray()
.add(targetAddressItem)
.add(amount)
.end()
//2nd output (optional)
if (change > 0) {
val myAddressItem =
CborDecoder(ByteArrayInputStream(transactionData.sourceAddress.decodeBase58()))
.decode()[0]
outputArray
.addArray()
.add(myAddressItem)
.add(change)
.end()
}
outputArray.end()
}
private fun calculateChange(amount: Long, fee: Long): Long {
val fullAmount = unspentOutputs.map { it.amount }.sum()
return fullAmount - (amount + fee)
}
fun getEstimateSize(transactionData: TransactionData, walletPublicKey: ByteArray): Int {
val dummyFeeValue = BigDecimal.valueOf(0.1)
val dummyFee = transactionData.amount.copy(value = dummyFeeValue)
val dummyAmount =
transactionData.amount.copy(value = transactionData.amount.value!! - dummyFeeValue)
val dummyTransactionData = transactionData.copy(
amount = dummyAmount,
fee = dummyFee
)
buildToSign(dummyTransactionData)
return buildToSend(ByteArray(64), walletPublicKey).size
}
companion object {
private const val PROTOCOL_MAGIC: Long = 764824073
}
}
class UnspentOutput(
val amount: Long,
val outputIndex: Long,
val hash: ByteArray
)

View file

@ -0,0 +1,70 @@
package com.tangem.blockchain.cardano
import android.util.Base64
import android.util.Log
import com.tangem.blockchain.cardano.network.CardanoAddressResponse
import com.tangem.blockchain.cardano.network.CardanoNetworkManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.encodeBase64NoWrap
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.tasks.TaskEvent
import java.math.BigDecimal
class CardanoWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = Blockchain.Cardano
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = CardanoTransactionBuilder()
private val networkManager = CardanoNetworkManager()
override suspend fun update() {
val response = networkManager.getInfo(address)
when (response) {
is Result.Success -> updateWallet(response.data)
is Result.Failure -> updateError(response.error)
}
}
private fun updateWallet(response: CardanoAddressResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance.toString()}")
currencyWallet.balances[AmountType.Coin]?.value =
response.balance.toBigDecimal().movePointLeft(blockchain.decimals.toInt())
transactionBuilder.unspentOutputs = response.unspentOutputs
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val transactionHash = transactionBuilder.buildToSign(transactionData)
when (val signerResponse = signer.sign(arrayOf(transactionHash), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature, walletPublicKey)
return networkManager.sendTransaction(transactionToSend.encodeBase64NoWrap())
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
val a = 0.155381
val b = 0.000043946
val size = transactionBuilder.getEstimateSize(
TransactionData(amount, null, source, destination), walletPublicKey
)
val fee = (a + b * size).toBigDecimal()
return Result.Success(listOf(Amount(blockchain.currency, fee, source, blockchain.decimals)))
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
package com.tangem.blockchain.cardano.network
import com.tangem.blockchain.cardano.UnspentOutput
import com.tangem.blockchain.cardano.network.adalite.AdaliteProvider
import com.tangem.blockchain.cardano.network.api.AdaliteApi
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_ADALITE
import com.tangem.blockchain.common.network.API_ADALITE_RESERVE
import com.tangem.blockchain.common.network.createRetrofitInstance
import retrofit2.HttpException
import java.io.IOException
class CardanoNetworkManager {
private val adaliteProvider by lazy {
val api = createRetrofitInstance(API_ADALITE)
.create(AdaliteApi::class.java)
AdaliteProvider(api)
}
private val adaliteReserveProvider by lazy {
val api = createRetrofitInstance(API_ADALITE_RESERVE)
.create(AdaliteApi::class.java)
AdaliteProvider(api)
}
private var provider = adaliteProvider
private fun changeProvider() {
provider = if (provider == adaliteProvider) adaliteReserveProvider else adaliteProvider
}
suspend fun getInfo(address: String): Result<CardanoAddressResponse> {
val result = provider.getInfo(address)
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.getInfo(address)
} else {
return result
}
}
}
}
suspend fun sendTransaction(transaction: String): SimpleResult {
val result = provider.sendTransaction(transaction)
when (result) {
is SimpleResult.Success -> return result
is SimpleResult.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.sendTransaction(transaction)
} else {
return result
}
}
}
}
}
data class CardanoAddressResponse(
val balance: Long,
val unspentOutputs: List<UnspentOutput>
)

View file

@ -0,0 +1,20 @@
package com.tangem.blockchain.cardano.network.api
import com.tangem.blockchain.cardano.network.adalite.AdaliteAddress
import com.tangem.blockchain.cardano.network.adalite.AdaliteSendBody
import com.tangem.blockchain.cardano.network.adalite.AdaliteUnspents
import retrofit2.http.*
import shadow.okhttp3.ResponseBody
interface AdaliteApi {
@GET("/api/addresses/summary/{address}")
suspend fun getAddress(@Path("address") address: String): AdaliteAddress
@Headers("Content-Type: application/json")
@POST("/api/bulk/addresses/utxo")
suspend fun getUnspents(@Body address: String): AdaliteUnspents
@Headers("Content-Type: application/json")
@POST("/api/v2/txs/signed")
suspend fun sendTransaction(@Body adaliteBody: AdaliteSendBody): ResponseBody // List<Any>?
}

View file

@ -0,0 +1,53 @@
package com.tangem.blockchain.cardano.network.adalite
import com.tangem.blockchain.cardano.network.CardanoAddressResponse
import com.tangem.blockchain.cardano.UnspentOutput
import com.tangem.blockchain.cardano.network.api.AdaliteApi
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.retryIO
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class AdaliteProvider(private val api: AdaliteApi) {
suspend fun getInfo(address: String): Result<CardanoAddressResponse> {
return try {
coroutineScope {
val addressDeferred = retryIO { async { api.getAddress(address) } }
val unspentsDeferred = retryIO { async { api.getUnspents(address) } }
val addressData = addressDeferred.await()
val unspents = unspentsDeferred.await()
val cardanoUnspents = unspents.data.map {
UnspentOutput(
it.amountData!!.amount!!,
it.outputIndex!!.toLong(),
it.hash!!.toByteArray()
)
}
Result.Success(
CardanoAddressResponse(
addressData.data!!.balanceData!!.amount!!,
cardanoUnspents
)
)
}
} catch (exception: Exception) {
Result.Failure(exception)
}
}
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
retryIO { api.sendTransaction(AdaliteSendBody(transaction)) }
SimpleResult.Success
} catch (exception: Exception) {
SimpleResult.Failure(exception)
}
}
}
data class AdaliteSendBody(val signedTransaction: String)

View file

@ -0,0 +1,49 @@
package com.tangem.blockchain.cardano.network.adalite
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AdaliteAddress(
@Json(name = "final_balance")
var data: AdaliteAddressData? = null
)
@JsonClass(generateAdapter = true)
data class AdaliteAddressData(
@Json(name = "caBalance")
var balanceData: BalanceData? = null
// @Json(name = "caTxList")
// var transactions: List<AdaliteTransaction>
)
@JsonClass(generateAdapter = true)
data class BalanceData(
@Json(name = "getCoin")
var amount: Long? = null
)
@JsonClass(generateAdapter = true)
data class AdaliteUnspents(
@Json(name = "Right")
var data: List<AdaliteUtxo>
)
@JsonClass(generateAdapter = true)
data class AdaliteUtxo(
@Json(name = "cuId")
var hash: String? = null,
@Json(name = "cuOutIndex")
var outputIndex: Int? = null,
@Json(name = "cuCoins")
var amountData: BalanceData? = null
)
//@JsonClass(generateAdapter = true)
//data class AdaliteTransaction(
// @Json(name = "ctbId")
// var hash: String? = null
//)

View file

@ -0,0 +1,74 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinAddressFactory
import com.tangem.blockchain.bitcoin.BitcoinAddressValidator
import com.tangem.blockchain.ethereum.EthereumAddressFactory
import com.tangem.blockchain.ethereum.EthereumAddressValidator
import com.tangem.blockchain.cardano.CardanoAddressFactory
import com.tangem.blockchain.cardano.CardanoAddressValidator
import com.tangem.blockchain.stellar.StellarAddressFactory
import com.tangem.blockchain.xrp.XrpAddressFactory
import com.tangem.blockchain.xrp.XrpAddressValidator
import java.math.BigDecimal
enum class Blockchain(
val id: String,
val currency: String,
val decimals: Byte,
val fullName: String,
val pendingTransactionTimeout: Int
) {
Unknown("", "", 0, "", 0),
Bitcoin("BTC", "BTC", 8, "Bitcoin", 0),
BitcoinTestnet("BTC", "BTC", 8, "Bitcoin Testnet", 0),
Ethereum("ETH", "ETH", 18, "Ethereum", 0),
Rootstock("", "", 18, "", 0),
Cardano("CARDANO", "ADA", 6, "Cardano", 0),
XRP("", "XRP", 6, "XRP Ledger", 0),
Binance("", "", 8, "", 0),
Stellar("XLM", "XLM", 7, "Stellar", 0);
fun roundingMode(): Int = when (this) {
Bitcoin, Ethereum, Rootstock, Binance -> BigDecimal.ROUND_DOWN
Cardano -> BigDecimal.ROUND_UP
else -> BigDecimal.ROUND_HALF_UP
}
fun makeAddress(walletPublicKey: ByteArray): String {
return when (this) {
Unknown -> throw Exception("unsupported blockchain")
Bitcoin -> BitcoinAddressFactory.makeAddress(walletPublicKey)
BitcoinTestnet -> BitcoinAddressFactory.makeAddress(walletPublicKey, testNet = true)
Ethereum -> EthereumAddressFactory.makeAddress(walletPublicKey)
// Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey)
Cardano -> CardanoAddressFactory.makeAddress(walletPublicKey)
XRP -> XrpAddressFactory.makeAddress(walletPublicKey)
// Binance -> BinanceAddressFactory.makeAddress(cardPublicKey)
Stellar -> StellarAddressFactory.makeAddress(walletPublicKey)
else -> throw Exception("unsupported blockchain")
}
}
fun validateAddress(address: String): Boolean {
return when (this) {
Unknown -> throw Exception("unsupported blockchain")
Bitcoin -> BitcoinAddressValidator.validate(address)
BitcoinTestnet -> BitcoinAddressValidator.validate(address, testNet = true)
Ethereum -> EthereumAddressValidator.validate(address)
// Rootstock -> RootstockAddressValidator.validate(address)
Cardano -> CardanoAddressValidator.validate(address)
XRP -> XrpAddressValidator.validate(address)
// Binance -> BinanceAddressValidator.validate(address)
// Stellar -> StellarAddressValidator.validate(address)
else -> throw Exception("unsupported blockchain")
}
}
companion object {
private val values = values()
fun fromId(id: String): Blockchain = values.find { it.id == id } ?: Unknown
fun fromName(name: String): Blockchain = values.find { it.name == name } ?: Unknown
fun fromCurrency(currency: String): Blockchain = values.find { it.currency == currency }
?: Unknown
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.blockchain.common
import java.math.BigDecimal
import java.util.*
interface Wallet {
val config: WalletConfig
val address: String
val exploreUrl: String?
val shareUrl: String?
}
class WalletConfig(
val allowFeeSelection: Boolean,
val allowFeeInclusion: Boolean,
var allowExtract: Boolean = false,
var allowLoad: Boolean = false
)
data class Amount(
val currencySymbol: String,
var value: BigDecimal? = null,
val address: String? = null,
val decimals: Byte,
val type: AmountType = AmountType.Coin
) {
constructor(
value: BigDecimal?,
blockchain: Blockchain,
address: String? = null,
type: AmountType = AmountType.Coin
) : this(blockchain.currency, value, address, blockchain.decimals, type)
}
data class TransactionData(
val amount: Amount,
val fee: Amount?,
val sourceAddress: String,
val destinationAddress: String,
var status: TransactionStatus = TransactionStatus.Unconfirmed,
var date: Calendar? = null
)
enum class AmountType { Coin, Token, Reserve }
enum class TransactionStatus { Confirmed, Unconfirmed }
enum class ValidationError { WrongAmount, WrongFee, WrongTotal }
interface TransactionValidator {
fun validateTransaction(amount: Amount, fee: Amount?): EnumSet<ValidationError>
}

View file

@ -0,0 +1,26 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.commands.SignResponse
import com.tangem.tasks.TaskEvent
import kotlinx.coroutines.flow.Flow
interface WalletManager {
var wallet: Wallet
val blockchain: Blockchain
suspend fun update()
}
interface TransactionSender {
suspend fun send(transactionData: TransactionData, signer: TransactionSigner) : SimpleResult
}
interface TransactionSigner {
suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse>
}
interface FeeProvider {
suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>>
}

View file

@ -0,0 +1,82 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.ethereum.Chain
import com.tangem.blockchain.ethereum.EthereumWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.stellar.StellarWalletManager
import com.tangem.blockchain.xrp.XrpWalletManager
import com.tangem.commands.Card
object WalletManagerFactory {
fun makeWalletManager(card: Card): WalletManager? {
val walletPublicKey: ByteArray = card.walletPublicKey ?: return null
val blockchainName: String = card.cardData?.blockchainName ?: return null
when {
blockchainName == Blockchain.Bitcoin.id -> {
return BitcoinWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true)
)
}
blockchainName == Blockchain.BitcoinTestnet.id -> {
return BitcoinWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true),
isTestNet = true
)
}
blockchainName == Blockchain.Ethereum.id -> {
val chain = Chain.Mainnet
return EthereumWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true),
chain = chain
)
}
blockchainName == Blockchain.Stellar.id -> {
val token = getToken(card)
return StellarWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, token == null),
token = token
)
}
blockchainName == Blockchain.Cardano.id -> {
return CardanoWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(false, true)
)
}
blockchainName == Blockchain.XRP.id -> {
return XrpWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true)
)
}
else -> return null
}
}
private fun getToken(card: Card): Token? {
val symbol = card.cardData?.tokenSymbol ?: return null
val contractAddress = card.cardData?.tokenContractAddress ?: return null
val decimals = card.cardData?.tokenDecimal ?: return null
return Token(symbol, contractAddress, decimals.toByte())
}
}
data class Token(
val symbol: String,
val contractAddress: String,
val decimals: Byte
)

View file

@ -0,0 +1,8 @@
package com.tangem.blockchain.common.extensions
import com.tangem.blockchain.common.Amount
import java.math.BigInteger
fun Amount.bigIntegerValue(): BigInteger? {
return this.value?.movePointRight(this.decimals.toInt())?.toBigInteger()
}

View file

@ -0,0 +1,11 @@
package com.tangem.blockchain.common.extensions
import org.bitcoinj.core.ECKey
import java.math.BigInteger
fun BigInteger.toCanonicalised(): BigInteger {
if (!this.isCanonical()) ECKey.CURVE.n - this
return this
}
fun BigInteger.isCanonical(): Boolean = this <= ECKey.HALF_CURVE_ORDER

View file

@ -0,0 +1,12 @@
package com.tangem.blockchain.common.extensions
import android.util.Base64
import org.bitcoinj.core.Base58
fun ByteArray.encodeBase58(): String {
return Base58.encode(this)
}
fun ByteArray.encodeBase64NoWrap(): String {
return Base64.encodeToString(this, Base64.NO_WRAP)
}

View file

@ -0,0 +1,66 @@
package com.tangem.blockchain.common.extensions
import com.tangem.CardManager
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.commands.SignResponse
import com.tangem.tasks.TaskEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun <T> retryIO(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block() // last attempt
}
//suspend fun <T: Any> handleRequest(requestFunc: suspend () -> T): Result<T> {
// return try {
// Result.success(requestFunc.invoke())
// } catch (he: HttpException) {
// Result.failure(he)
//// HttpException
//// SocketTimeoutException
//// IOException
// }
//}
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
sealed class SimpleResult {
object Success : SimpleResult()
data class Failure(val error: Throwable?) : SimpleResult()
}
class Signer(private val cardManager: CardManager) : TransactionSigner {
override suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse> = coroutineScope {
async {
suspendCancellableCoroutine<TaskEvent<SignResponse>> { continuation ->
cardManager.sign(hashes, cardId) { if (continuation.isActive) continuation.resume(it) }
}
}.await()
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.blockchain.common.extensions
import org.bitcoinj.core.AddressFormatException
import org.bitcoinj.core.Base58
fun String.decodeBase58(): ByteArray? {
return try {
Base58.decode(this)
} catch (exception: AddressFormatException) {
null
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.blockchain.common.network
import com.tangem.blockchain.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder().apply {
if (BuildConfig.DEBUG) addInterceptor(createHttpLoggingInterceptor())
}.build()
}
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
return logging
}
fun createRetrofitInstance(baseUrl: String): Retrofit =
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.build()
const val API_TANGEM = "https://verify.tangem.com/"
const val API_COINMARKETCAP = "https://pro-api.coinmarketcap.com/"
const val API_INFURA = "https://mainnet.infura.io/"
const val API_SOCHAIN_V2 = "https://chain.so/"
const val API_ESTIMATEFEE = "https://estimatefee.com/"
const val API_UPDATE_VERSION = "https://raw.githubusercontent.com/"
const val API_ROOTSTOCK = "https://public-node.rsk.co/"
const val API_BLOCKCYPHER = "https://api.blockcypher.com/"
const val API_BINANCE = "https://dex.binance.org/"
const val API_BINANCE_TESTNET = "https://testnet-dex.binance.org/"
const val API_MATIC_TESTNET = "https://testnet2.matic.network/"
const val API_STELLAR = "https://horizon.stellar.org/"
const val API_STELLAR_RESERVE = "https://horizon.sui.li/"
const val API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/"
const val API_BLOCKCHAIN_INFO = "https://blockchain.info/"
const val API_ADALITE = "https://explorer2.adalite.io"
const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com"
const val API_RIPPLED = "https://s1.ripple.com:51234"
const val API_RIPPLED_RESERVE = "https://s2.ripple.com:51234"

View file

@ -0,0 +1,33 @@
package com.tangem.blockchain.ethereum
import org.kethereum.crypto.toAddress
import org.kethereum.functions.isValid
import org.kethereum.model.Address
import org.kethereum.model.PublicKey
class EthereumAddressFactory {
companion object {
fun makeAddress(walletPublicKey: ByteArray): String =
PublicKey(walletPublicKey.sliceArray(1..64)).toAddress().hex
}
}
class EthereumAddressValidator {
companion object {
fun validate(address: String): Boolean = Address(address).isValid()
}
}
enum class Chain(val id: Int) {
Mainnet(1),
Morden(2),
Ropsten(3),
Rinkeby(4),
RootstockMainnet(30),
RootstockTestnet(31),
Kovan(42),
EthereumClassicMainnet(61),
EthereumClassicTestnet(62),
Geth_private_chains(1337),
MaticTestnet(8995);
}

View file

@ -0,0 +1,144 @@
package com.tangem.blockchain.ethereum
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.ethereum.network.EthereumResponse
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.common.extensions.toHexString
import com.tangem.tasks.TaskEvent
import org.kethereum.DEFAULT_GAS_LIMIT
import org.kethereum.crypto.api.ec.ECDSASignature
import org.kethereum.crypto.determineRecId
import org.kethereum.crypto.impl.ec.canonicalise
import org.kethereum.functions.encodeRLP
import org.kethereum.keccakshortcut.keccak
import org.kethereum.model.*
import java.math.BigDecimal
import java.math.BigInteger
class EthereumWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
chain: Chain,
walletConfig: WalletConfig
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain: Blockchain = Blockchain.Ethereum
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val builder = EthereumTransactionBuilder(chain)
private val networkManager = EthereumNetworkManager()
private var pendingTxCount = -1L
private var txCount = -1L
override suspend fun update() {
val result = networkManager.getInfo(address, currencyWallet.balances[AmountType.Token]?.address)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)
}
}
private fun updateWallet(data: EthereumResponse) {
currencyWallet.balances[AmountType.Coin]?.value = data.balance
currencyWallet.balances[AmountType.Token]?.value = data.tokenBalance
txCount = data.txCount
pendingTxCount = data.pendingTxCount
if (txCount == pendingTxCount) {
currencyWallet.pendingTransactions.forEach { it.status = TransactionStatus.Confirmed }
} else if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
}
}
private fun updateError(error: Throwable?) {
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val transactionToSign = builder.buildToSign(transactionData, txCount.toBigInteger())
?: return SimpleResult.Failure(Exception("Not enough data"))
when (val signerResponse = signer.sign(transactionToSign.hashes.toTypedArray(), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = builder.buildToSend(signerResponse.data.signature, transactionToSign, walletPublicKey)
return networkManager.sendTransaction(String.format("0x%s", transactionToSend.toHexString()))
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
val result = networkManager.getFee(getGasLimit(amount).value)
when (result) {
is Result.Success -> {
val feeValues: List<BigDecimal> = result.data
return Result.Success(
feeValues.map { Amount(blockchain.currency, it, address, blockchain.decimals) })
}
is Result.Failure -> return result
}
}
}
private class EthereumTransactionBuilder(private val chain: Chain) {
fun buildToSign(transactionData: TransactionData, nonce: BigInteger?): TransactionToSign? {
val amount: BigDecimal = transactionData.amount.value ?: return null
val transactionFee: BigDecimal = transactionData.fee?.value ?: return null
val value = amount.movePointRight(transactionData.amount.decimals.toInt()).toBigInteger()
val fee = transactionFee.movePointRight(transactionData.fee.decimals.toInt()).toBigInteger()
val transaction = createTransactionWithDefaults(
from = Address(transactionData.sourceAddress),
to = Address(transactionData.destinationAddress),
value = value,
gasPrice = fee.divide(DEFAULT_GAS_LIMIT),
gasLimit = DEFAULT_GAS_LIMIT,
nonce = nonce,
chain = ChainId(chain.id.toLong())
)
val hash = transaction.encodeRLP(SignatureData(v = chain.id.toBigInteger())).keccak()
return TransactionToSign(transaction, listOf(hash))
}
fun buildToSend(signature: ByteArray, transactionToSign: TransactionToSign, walletPublicKey: ByteArray): ByteArray {
val r = BigInteger(1, signature.copyOfRange(0, 32))
val s = BigInteger(1, signature.copyOfRange(32, 64))
val ecdsaSignature = ECDSASignature(r, s).canonicalise()
val recId = ecdsaSignature.determineRecId(transactionToSign.hashes[0], PublicKey(walletPublicKey.sliceArray(1..64)))
val v = (recId + 27 + 8 + (chain.id * 2)).toBigInteger()
val signatureData = SignatureData(ecdsaSignature.r, ecdsaSignature.s, v)
return transactionToSign.transaction.encodeRLP(signatureData)
}
}
private class TransactionToSign(val transaction: Transaction, val hashes: List<ByteArray>)
enum class GasLimit(val value: Long) {
Default(21000),
Token(60000),
High(300000)
}
private fun getGasLimit(amount: Amount): GasLimit {
return when (amount.currencySymbol) {
Blockchain.Ethereum.currency -> GasLimit.Default
"DGX" -> GasLimit.High
"CGT" -> GasLimit.High
else -> GasLimit.Token
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.blockchain.ethereum.network
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.retryIO
import com.tangem.blockchain.common.network.API_INFURA
import com.tangem.blockchain.common.network.createRetrofitInstance
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.kethereum.ETH_IN_WEI
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
class EthereumNetworkManager {
private val api: InfuraApi by lazy {
createRetrofitInstance(API_INFURA).create(InfuraApi::class.java)
}
private val provider: InfuraProvider by lazy { InfuraProvider(api) }
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val response = retryIO { provider.sendTransaction(transaction) }
if (response.error == null) {
SimpleResult.Success
} else {
SimpleResult.Failure(Exception("Code: ${(response.error.code)}, ${(response.error.message)}"))
}
} catch (error: Exception) {
SimpleResult.Failure(error)
}
}
suspend fun getFee(gasLimit: Long): Result<List<BigDecimal>> {
return try {
Result.Success(
provider.getGasPrice().result!!.parseFee(gasLimit)
)
} catch (error: Exception) {
Result.Failure(error)
}
}
suspend fun getInfo(address: String, contractAddress: String? = null): Result<EthereumResponse> {
return try {
coroutineScope {
val balanceResponse = retryIO { async { provider.getBalance(address) } }
val txCountResponse = retryIO { async { provider.getTxCount(address) } }
val pendingTxCountResponse = retryIO { async { provider.getPendingTxCount(address) } }
var tokenBalanceResponse: Deferred<InfuraResponse>? = null
if (contractAddress != null) {
tokenBalanceResponse = retryIO { async { provider.getTokenBalance(address, contractAddress) } }
}
Result.Success(EthereumResponse(
balanceResponse.await().result!!.parseAmount(),
tokenBalanceResponse?.await()?.result?.parseAmount(),
txCountResponse.await().result?.responseToNumber()?.toLong() ?: 0,
pendingTxCountResponse.await().result?.responseToNumber()?.toLong() ?: 0
))
}
} catch (error: Exception) {
Result.Failure(error)
}
}
private fun String.parseFee(gasLimit: Long): List<BigDecimal> {
val gasPrice = this.responseToNumber().toBigDecimal()
val minFee = gasPrice.multiply(gasLimit.toBigDecimal())
val normalFee = minFee.multiply(BigDecimal(1.2))
val priorityFee = minFee.multiply(BigDecimal(1.5))
return listOf(
minFee.convertFeeToEth(),
normalFee.convertFeeToEth(),
priorityFee.convertFeeToEth()
)
}
private fun String.responseToNumber(): BigInteger = this.substring(2).toBigInteger(16)
private fun String.parseAmount(): BigDecimal =
this.responseToNumber().toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
private fun BigDecimal.convertFeeToEth(): BigDecimal {
return this.divide(ETH_IN_WEI.toBigDecimal())
.setScale(12, Blockchain.Ethereum.roundingMode()).stripTrailingZeros()
}
}
data class EthereumResponse(
val balance: BigDecimal,
val tokenBalance: BigDecimal?,
val txCount: Long,
val pendingTxCount: Long
)

View file

@ -0,0 +1,31 @@
package com.tangem.blockchain.ethereum.network
import com.squareup.moshi.JsonClass
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface InfuraApi {
@Headers("Content-Type: application/json")
@POST("v3/613a0b14833145968b1f656240c7d245")
suspend fun postToInfura(@Body body: InfuraBody?): InfuraResponse
}
@JsonClass(generateAdapter = true)
data class InfuraBody(
val jsonrpc: String = "2.0",
val id: Int = 67,
val method: String? = null,
val params: List<Any> = listOf()
)
data class EthCallParams(private val data: String, private val to: String)
enum class InfuraMethod(val value: String) {
GET_BALANCE("eth_getBalance"),
GET_TRANSACTION_COUNT("eth_getTransactionCount"),
GET_PENDING_COUNT("eth_getPendingCount"),
CALL("eth_call"),
SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
GAS_PRICE("eth_gasPrice")
}

View file

@ -0,0 +1,39 @@
package com.tangem.blockchain.ethereum.network
class InfuraProvider(private val api: InfuraApi) {
suspend fun getBalance(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_BALANCE, address))
suspend fun getTokenBalance(address: String, contractAddress: String) = api.postToInfura(createInfuraBody(InfuraMethod.CALL, address, contractAddress))
suspend fun getTxCount(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_TRANSACTION_COUNT, address))
suspend fun getPendingTxCount(address: String) = api.postToInfura(createInfuraBody(InfuraMethod.GET_PENDING_COUNT, address))
suspend fun getGasPrice() = api.postToInfura(createInfuraBody(InfuraMethod.GAS_PRICE))
suspend fun sendTransaction(transaction: String) = api.postToInfura(createInfuraBody(InfuraMethod.SEND_RAW_TRANSACTION, transaction = transaction))
}
private fun createInfuraBody(
method: InfuraMethod,
address: String? = null,
contractAddress: String? = null,
transaction: String? = null): InfuraBody {
return when (method) {
InfuraMethod.GET_BALANCE ->
InfuraBody(method = InfuraMethod.GET_BALANCE.value, params = listOf(address ?: "", "latest"))
InfuraMethod.GET_TRANSACTION_COUNT ->
InfuraBody(method = InfuraMethod.GET_TRANSACTION_COUNT.value, params = listOf(address ?: "", "latest"))
InfuraMethod.GET_PENDING_COUNT ->
InfuraBody(method = InfuraMethod.GET_TRANSACTION_COUNT.value, params = listOf(address ?: "", "pending"))
InfuraMethod.GAS_PRICE ->
InfuraBody(method = InfuraMethod.GAS_PRICE.value)
InfuraMethod.SEND_RAW_TRANSACTION ->
InfuraBody(method = InfuraMethod.SEND_RAW_TRANSACTION.value, params = listOf(transaction ?: ""))
InfuraMethod.CALL -> {
InfuraBody(
method = InfuraMethod.CALL.value,
params = listOf(EthCallParams(
"0x70a08231000000000000000000000000" + address?.substring(2), contractAddress
?: ""),
"latest"
))
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.blockchain.ethereum.network
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class InfuraResponse(
@Json(name = "jsonrpc")
val jsonrpc: String = "",
@Json(name = "id")
val id: Int? = null,
@Json(name = "result")
val result: String? = null,
@Json(name = "error")
val error: InfuraError? = null
)
@JsonClass(generateAdapter = true)
data class InfuraError(
@Json(name = "code")
val code: Int? = null,
@Json(name = "message")
val message: String? = null
)

View file

@ -0,0 +1,24 @@
package com.tangem.blockchain.stellar
import org.stellar.sdk.KeyPair
class StellarAddressFactory {
companion object {
fun makeAddress(cardPublicKey: ByteArray): String {
val kp = KeyPair.fromPublicKey(cardPublicKey)
return kp.accountId
}
}
}
class StellarAddressValidator {
companion object {
fun validate(address: String): Boolean {
return try {
KeyPair.fromAccountId(address) != null
} catch (exception: IllegalArgumentException) {
false
}
}
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.blockchain.stellar
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_STELLAR
import com.tangem.blockchain.common.network.API_STELLAR_TESTNET
import com.tangem.blockchain.stellar.StellarWalletManager.Companion.STROOPS_IN_XLM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.stellar.sdk.Network
import org.stellar.sdk.Server
import org.stellar.sdk.Transaction
import org.stellar.sdk.requests.ErrorResponse
import java.io.IOException
import java.math.BigDecimal
class StellarNetworkManager(isTestNet: Boolean) {
val network: Network = if (isTestNet) Network.TESTNET else Network.PUBLIC
private val stellarServer by lazy {
Server(if (isTestNet) API_STELLAR_TESTNET else API_STELLAR)
}
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val response = stellarServer.submitTransaction(Transaction.fromEnvelopeXdr(transaction, network))
if (response.isSuccess) {
SimpleResult.Success
} else {
val trResult: String? = response.extras?.resultCodes?.transactionResultCode +
(response.extras?.resultCodes?.operationsResultCodes?.getOrNull(0) ?: "")
SimpleResult.Failure(Exception(trResult ?: "transaction failed"))
}
} catch (error: Exception) {
SimpleResult.Failure(error)
}
}
suspend fun checkIsAccountCreated(address: String): Boolean {
try {
stellarServer.accounts().account(address)
return true
} catch (errorResponse: ErrorResponse) {
if (errorResponse.code == 404) return false
return false
} catch (exception: IOException) {
return false
}
}
suspend fun getInfo(accountId: String, assetCode: String? = null): Result<StellarResponse> {
return try {
coroutineScope {
val accountResponseDefered = async(Dispatchers.IO) { stellarServer.accounts().account(accountId) }
val ledgerResponseDeferred = async(Dispatchers.IO) {
val latestLedger: Int = stellarServer.root().coreLatestLedger
stellarServer.ledgers().ledger(latestLedger.toLong())
}
val accountResponse = accountResponseDefered.await()
val balance = accountResponse.balances
.find { it.assetType == "native" }
?.balance?.toBigDecimal()
?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found"))
val assetBalance = if (assetCode == null) {
null
} else {
accountResponse.balances
.find { it.assetType != "native" && it.assetCode == assetCode }
?.balance?.toBigDecimal()
?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found"))
}
val sequence = accountResponse.sequenceNumber
val ledgerResponse = ledgerResponseDeferred.await()
val baseFee = ledgerResponse.baseFeeInStroops.toBigDecimal().divide(STROOPS_IN_XLM)
val baseReserve = ledgerResponse.baseReserveInStroops.toBigDecimal().divide(STROOPS_IN_XLM)
Result.Success(StellarResponse(
baseFee,
baseReserve,
assetBalance,
balance,
sequence
))
}
} catch (error: Exception) {
Result.Failure(error)
}
}
}
data class StellarResponse(
val baseFee: BigDecimal,
val baseReserve: BigDecimal,
val assetBalance: BigDecimal?,
val balance: BigDecimal,
val sequence: Long
)

View file

@ -0,0 +1,84 @@
package com.tangem.blockchain.stellar
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.stellar.StellarWalletManager.Companion.BASE_FEE
import com.tangem.blockchain.stellar.StellarWalletManager.Companion.STROOPS_IN_XLM
import com.tangem.common.extensions.hexToBytes
import org.stellar.sdk.*
import org.stellar.sdk.xdr.AccountID
import org.stellar.sdk.xdr.DecoratedSignature
import org.stellar.sdk.xdr.Signature
import org.stellar.sdk.xdr.SignatureHint
import java.util.*
class StellarTransactionBuilder(private val newtorkManager: StellarNetworkManager, private val publicKey: ByteArray) {
private lateinit var transaction: Transaction
suspend fun buildToSign(transactionData: TransactionData, sequence: Long, fee: Int): List<ByteArray> {
val destinationKeyPair = KeyPair.fromAccountId(transactionData.destinationAddress)
val sourceKeyPair = KeyPair.fromAccountId(transactionData.sourceAddress)
if (transactionData.amount.type == AmountType.Coin) {
val operation = if (newtorkManager.checkIsAccountCreated(transactionData.sourceAddress)) {
PaymentOperation.Builder(destinationKeyPair.accountId,
AssetTypeNative(),
transactionData.amount.value.toString())
.build()
} else {
CreateAccountOperation.Builder(destinationKeyPair.accountId, transactionData.amount.value.toString()).build()
}
return serializeOperation(operation, sourceKeyPair, sequence, fee)
} else if (transactionData.amount.type == AmountType.Token) {
val keyPair = KeyPair.fromAccountId(transactionData.amount.address)
val asset = Asset.createNonNativeAsset(transactionData.amount.currencySymbol, keyPair.accountId)
val operation: Operation = if (transactionData.amount.value != null) {
PaymentOperation.Builder(
destinationKeyPair.accountId,
asset,
transactionData.amount.value!!.toPlainString())
.build()
} else {
ChangeTrustOperation.Builder(asset, "900000000000.0000000")
.setSourceAccount(sourceKeyPair.accountId)
.build()
}
return serializeOperation(operation, sourceKeyPair, sequence, fee)
} else {
return emptyList()
}
}
private fun serializeOperation(
operation: Operation, sourceKeyPair: KeyPair,
sequence: Long, fee: Int
): List<ByteArray> {
val accountID = AccountID()
accountID.accountID = sourceKeyPair.xdrPublicKey
val currentTime = Calendar.getInstance().timeInMillis / 1000
val minTime = 0L
val maxTime = currentTime + 120
transaction = Transaction.Builder(
Account(sourceKeyPair.accountId, sequence), newtorkManager.network)
.addOperation(operation)
.addTimeBounds(TimeBounds(minTime, maxTime))
.setOperationFee(fee)
.build()
return listOf<ByteArray>(transaction.hash())
}
fun buildToSend(signature: ByteArray): String {
val hint = publicKey.takeLast(4).toByteArray()
val decoratedSignature = DecoratedSignature().apply {
this.hint = SignatureHint().apply { signatureHint = hint }
this.signature = Signature().apply { this.signature = signature }
}
transaction.signatures.add(decoratedSignature)
return transaction.toEnvelopeXdrBase64()
}
}

View file

@ -0,0 +1,95 @@
package com.tangem.blockchain.stellar
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.tasks.TaskEvent
import java.math.BigDecimal
import java.util.*
class StellarWalletManager(
private val cardId: String,
walletPublicKey: ByteArray,
walletConfig: WalletConfig,
token: Token? = null,
isTestNet: Boolean = false
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain: Blockchain = Blockchain.Stellar
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val networkManager = StellarNetworkManager(isTestNet)
private val builder = StellarTransactionBuilder(networkManager, walletPublicKey)
private var baseFee = BASE_FEE
private var baseReserve = BASE_RESERVE
private var sequence = 0L
init {
if (token != null) currencyWallet.balances[AmountType.Token] =
Amount(
token.symbol,
null,
token.contractAddress,
token.decimals,
AmountType.Token)
}
override suspend fun update() {
val result = networkManager.getInfo(address, currencyWallet.balances[AmountType.Token]?.address)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)
}
}
private fun updateWallet(data: StellarResponse) {
currencyWallet.balances[AmountType.Coin]?.value = data.balance
currencyWallet.balances[AmountType.Token]?.value = data.assetBalance
currencyWallet.balances[AmountType.Reserve]?.value = data.baseReserve
sequence = data.sequence
baseFee = data.baseFee
baseReserve = data.baseReserve
val currentTime = Calendar.getInstance().timeInMillis
currencyWallet.pendingTransactions.forEach { transaction ->
if (transaction.date?.timeInMillis ?: 0 - currentTime > 10) {
transaction.status = TransactionStatus.Confirmed
}
}
}
private fun updateError(error: Throwable?) {
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val hashes = builder.buildToSign(transactionData, sequence, baseFee.toStroops())
when (val signerResponse = signer.sign(hashes.toTypedArray(), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = builder.buildToSend(signerResponse.data.signature)
return networkManager.sendTransaction(transactionToSend)
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
return Result.Success(listOf(
Amount(baseFee, blockchain)
))
}
private fun BigDecimal.toStroops(): Int {
return this.multiply(STROOPS_IN_XLM).toInt()
}
companion object {
val STROOPS_IN_XLM = 10000000.toBigDecimal()
val BASE_FEE = 0.00001.toBigDecimal()
val BASE_RESERVE = 0.5.toBigDecimal()
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.blockchain.wallets
import com.tangem.blockchain.common.*
import java.util.*
class CurrencyWallet(
override val config: WalletConfig,
override val address: String,
override val exploreUrl: String? = null,
override val shareUrl: String? = null,
val pendingTransactions: MutableList<TransactionData> = mutableListOf(),
val balances: MutableMap<AmountType, Amount> = mutableMapOf(),
val isTestnet: Boolean = false
) : Wallet, TransactionValidator {
override fun validateTransaction(amount: Amount, fee: Amount?): EnumSet<ValidationError> {
TODO("not implemented")
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.blockchain.xrp
import com.ripple.encodings.addresses.Addresses
import com.tangem.common.extensions.calculateRipemd160
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toCompressedPublicKey
class XrpAddressFactory {
companion object {
fun makeAddress(walletPublicKey: ByteArray): String {
val canonicalPublicKey = canonizePublicKey(walletPublicKey)
val publicKeyHash = canonicalPublicKey.calculateSha256().calculateRipemd160()
return Addresses.encodeAccountID(publicKeyHash)
}
fun canonizePublicKey(publicKey: ByteArray): ByteArray {
val compressedPublicKey = publicKey.toCompressedPublicKey()
return if (compressedPublicKey.size == 32) {
byteArrayOf(0xED.toByte()) + compressedPublicKey
} else {
compressedPublicKey
}
}
}
}
class XrpAddressValidator {
companion object {
fun validate(address: String): Boolean {
return try {
Addresses.decodeAccountID(address)
address.startsWith("r")
} catch (excpetion: Exception) {
false
}
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.blockchain.xrp
import com.ripple.core.coretypes.AccountID
import com.ripple.core.coretypes.Amount
import com.ripple.core.coretypes.uint.UInt32
import com.ripple.crypto.ecdsa.ECDSASignature
import com.ripple.utils.HashUtils
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.extensions.bigIntegerValue
import com.tangem.blockchain.xrp.override.XrpPayment
import com.tangem.blockchain.xrp.override.XrpSignedTransaction
import org.bitcoinj.core.ECKey
import java.math.BigInteger
class XrpTransactionBuilder(walletPublicKey: ByteArray) {
private val canonicalPublicKey = XrpAddressFactory.canonizePublicKey(walletPublicKey)
var sequence: Long? = null
private var transaction: XrpSignedTransaction? = null
fun buildToSign(transactionData: TransactionData): ByteArray {
val payment = XrpPayment()
payment.putTranslated(AccountID.Account, transactionData.sourceAddress)
payment.putTranslated(AccountID.Destination, transactionData.destinationAddress)
payment.putTranslated(Amount.Amount, transactionData.amount.bigIntegerValue().toString())
payment.putTranslated(UInt32.Sequence, sequence)
payment.putTranslated(Amount.Fee, transactionData.fee!!.bigIntegerValue().toString())
transaction = payment.prepare(canonicalPublicKey)
return if (canonicalPublicKey[0] == 0xED.toByte()) {
transaction!!.signingData
} else {
HashUtils.halfSha512(transaction!!.signingData)
}
}
fun buildToSend(signature: ByteArray): String {
if (canonicalPublicKey[0] == 0xED.toByte()) {
transaction!!.addSign(signature)
} else {
val derSignature = encodeDerSignature(signature)
transaction!!.addSign(derSignature)
}
return transaction!!.tx_blob
}
private fun encodeDerSignature(signature: ByteArray): ByteArray {
val r = BigInteger(1, signature.copyOfRange(0, 32))
val s = BigInteger(1, signature.copyOfRange(32, 64))
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
val ecdsaSignature = ECDSASignature(r, canonicalS)
return ecdsaSignature.encodeToDER()
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.blockchain.xrp
import android.util.Log
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.blockchain.xrp.network.XrpInfoResponse
import com.tangem.blockchain.xrp.network.XrpNetworkManager
import com.tangem.tasks.TaskEvent
class XrpWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = Blockchain.XRP
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = XrpTransactionBuilder(walletPublicKey)
private val networkManager = XrpNetworkManager()
override suspend fun update() {
val result = networkManager.getInfo(address)
when (result) {
is Result.Success -> updateWallet(result.data)
is Result.Failure -> updateError(result.error)
}
}
private fun updateWallet(response: XrpInfoResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
currencyWallet.balances[AmountType.Reserve]?.value = response.reserveBase
if (!response.accountFound) {
updateError(Exception("Account not found")) //TODO rework, add reserve
return
}
currencyWallet.balances[AmountType.Coin]?.value = response.balance - response.reserveBase
transactionBuilder.sequence = response.sequence
if (response.hasUnconfirmed) {
if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
}
} else {
currencyWallet.pendingTransactions.clear()
}
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val transactionHash = transactionBuilder.buildToSign(transactionData)
when (val signerResponse = signer.sign(arrayOf(transactionHash), cardId)) {
is TaskEvent.Event -> {
val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature)
return networkManager.sendTransaction(transactionToSend)
}
is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error)
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
val result = networkManager.getFee()
when (result) {
is Result.Failure -> return result
is Result.Success -> return Result.Success(listOf(
Amount(result.data.minimalFee, blockchain),
Amount(result.data.normalFee, blockchain),
Amount(result.data.priorityFee, blockchain)
))
}
}
}

View file

@ -0,0 +1,92 @@
package com.tangem.blockchain.xrp.network
import com.tangem.blockchain.bitcoin.network.BitcoinFee
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_RIPPLED
import com.tangem.blockchain.common.network.API_RIPPLED_RESERVE
import com.tangem.blockchain.common.network.createRetrofitInstance
import com.tangem.blockchain.xrp.network.rippled.RippledApi
import com.tangem.blockchain.xrp.network.rippled.RippledProvider
import retrofit2.HttpException
import java.io.IOException
import java.math.BigDecimal
class XrpNetworkManager {
private val rippledProvider by lazy {
val api = createRetrofitInstance(API_RIPPLED)
.create(RippledApi::class.java)
RippledProvider(api)
}
private val rippledReserveProvider by lazy {
val api = createRetrofitInstance(API_RIPPLED_RESERVE)
.create(RippledApi::class.java)
RippledProvider(api)
}
var provider = rippledProvider
private fun changeProvider() {
provider = if (provider == rippledProvider) rippledReserveProvider else rippledProvider
}
suspend fun getInfo(address: String): Result<XrpInfoResponse> {
val result = provider.getInfo(address)
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.getInfo(address)
} else {
return result
}
}
}
}
suspend fun sendTransaction(transaction: String): SimpleResult {
val result = provider.sendTransaction(transaction)
when (result) {
is SimpleResult.Success -> return result
is SimpleResult.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.sendTransaction(transaction)
} else {
return result
}
}
}
}
suspend fun getFee(): Result<XrpFeeResponse> {
val result = provider.getFee()
when (result) {
is Result.Success -> return result
is Result.Failure -> {
if (result.error is IOException || result.error is HttpException) {
changeProvider()
return provider.getFee()
} else {
return result
}
}
}
}
}
data class XrpInfoResponse(
val balance: BigDecimal = BigDecimal.ZERO,
val sequence: Long = 0,
val hasUnconfirmed: Boolean = false,
val reserveBase: BigDecimal,
val accountFound: Boolean = true
)
data class XrpFeeResponse(
val minimalFee: BigDecimal,
val normalFee: BigDecimal,
val priorityFee: BigDecimal
)

View file

@ -0,0 +1,38 @@
package com.tangem.blockchain.xrp.network.rippled
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface RippledApi {
@Headers("Content-Type: application/json")
@POST("./")
suspend fun getAccount(@Body rippledBody: RippledBody): RippledAccountResponse
@Headers("Content-Type: application/json")
@POST("./")
suspend fun getServerState(@Body rippledBody: RippledBody = serverStateBody): RippledStateResponse
@Headers("Content-Type: application/json")
@POST("./")
suspend fun getFee(@Body rippledBody: RippledBody = feeBody): RippledFeeResponse
@Headers("Content-Type: application/json")
@POST("./")
suspend fun submitTransaction(@Body rippledBody: RippledBody): RippledSubmitResponse
}
enum class RippledMethod(val value: String) {
ACCOUNT_INFO("account_info"),
SERVER_STATE("server_state"),
FEE("fee"),
SUBMIT("submit")
}
data class RippledBody(
val method: String,
val params: HashMap<String, String> = HashMap() //TODO =null?
)
val serverStateBody = RippledBody(RippledMethod.SERVER_STATE.value)
val feeBody = RippledBody(RippledMethod.FEE.value)

View file

@ -0,0 +1,101 @@
package com.tangem.blockchain.xrp.network.rippled
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.retryIO
import com.tangem.blockchain.xrp.network.XrpFeeResponse
import com.tangem.blockchain.xrp.network.XrpInfoResponse
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
class RippledProvider(private val api: RippledApi) {
private val decimals = Blockchain.XRP.decimals.toInt()
suspend fun getInfo(address: String): Result<XrpInfoResponse> {
return try {
coroutineScope {
val accountBody = makeAccountBody(address, validated = true)
val accountDeferred = retryIO { async { api.getAccount(accountBody) } }
val unconfirmedBody = makeAccountBody(address, validated = false)
val unconfirmedDeferred = retryIO { async { api.getAccount(unconfirmedBody) } }
val stateDeferred = retryIO { async { api.getServerState() } }
val accountData = accountDeferred.await()
val unconfirmedData = unconfirmedDeferred.await()
val serverState = stateDeferred.await()
val reserveBase = serverState.result!!.state!!.validatedLedger!!.reserveBase!!
.toBigDecimal().movePointLeft(decimals)
if (accountData.result!!.errorCode == 19) {
Result.Success(XrpInfoResponse(
reserveBase = reserveBase,
accountFound = false
))
} else {
val confirmedBalance =
accountData.result!!.accountData!!.balance!!.toBigDecimal()
.movePointLeft(decimals)
val unconfirmedBalance =
unconfirmedData.result!!.accountData!!.balance!!.toBigDecimal()
.movePointLeft(decimals)
Result.Success(XrpInfoResponse(
balance = confirmedBalance,
sequence = accountData.result!!.accountData!!.sequence!!,
hasUnconfirmed = confirmedBalance != unconfirmedBalance,
reserveBase = reserveBase
))
}
}
} catch (exception: Exception) {
Result.Failure(exception)
}
}
suspend fun getFee(): Result<XrpFeeResponse> {
return try {
val feeData = retryIO { api.getFee() }
Result.Success(XrpFeeResponse(
feeData.result!!.feeData!!.minimalFee!!.toBigDecimal().movePointLeft(decimals),
feeData.result!!.feeData!!.normalFee!!.toBigDecimal().movePointLeft(decimals),
feeData.result!!.feeData!!.priorityFee!!.toBigDecimal().movePointLeft(decimals)
))
} catch (exception: Exception) {
Result.Failure(exception)
}
}
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val submitBody = makeSubmitBody(transaction)
val submitData = retryIO { api.submitTransaction(submitBody) }
if (submitData.result!!.resultCode == 0) {
SimpleResult.Success
} else {
SimpleResult.Failure(Exception(submitData.result!!.resultMessage
?: submitData.result!!.errorException))
}
} catch (exception: Exception) {
SimpleResult.Failure(exception)
}
}
}
private fun makeAccountBody(address: String, validated: Boolean): RippledBody {
val params = HashMap<String, String>()
params["account"] = address
params["ledger_index"] = if (validated) "validated" else "current"
return RippledBody(RippledMethod.ACCOUNT_INFO.value, params)
}
private fun makeSubmitBody(transaction: String): RippledBody {
val params = HashMap<String, String>()
params["tx_blob"] = transaction
return RippledBody(RippledMethod.SUBMIT.value, params)
}

View file

@ -0,0 +1,103 @@
package com.tangem.blockchain.xrp.network.rippled
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// Rippled account
@JsonClass(generateAdapter = true)
data class RippledAccountResponse(
@Json(name = "result")
var result: RippledAccountResult? = null
)
@JsonClass(generateAdapter = true)
data class RippledAccountResult(
@Json(name = "account_data")
var accountData: RippledAccountData? = null,
@Json(name = "error_code")
var errorCode: Int? = null
)
@JsonClass(generateAdapter = true)
data class RippledAccountData(
@Json(name = "Balance")
var balance: String? = null,
@Json(name = "Sequence")
var sequence: Long? = null
)
// Rippled state
@JsonClass(generateAdapter = true)
data class RippledStateResponse(
@Json(name = "result")
var result: RippledStateResult? = null
)
@JsonClass(generateAdapter = true)
data class RippledStateResult(
@Json(name = "state")
var state: RippledState? = null
)
@JsonClass(generateAdapter = true)
data class RippledState(
@Json(name = "validated_ledger")
var validatedLedger: RippledLedger? = null
)
@JsonClass(generateAdapter = true)
data class RippledLedger(
@Json(name = "reserve_base")
var reserveBase: Long? = null
)
// Rippled fee
@JsonClass(generateAdapter = true)
data class RippledFeeResponse(
@Json(name = "result")
var result: RippledFeeResult? = null
)
@JsonClass(generateAdapter = true)
data class RippledFeeResult(
@Json(name = "drops")
var feeData: RippledFeeData? = null
)
@JsonClass(generateAdapter = true)
data class RippledFeeData(
//enough to put tx to queue
@Json(name = "minimum_fee")
var minimalFee: String? = null,
//enough to put tx to current ledger
@Json(name = "open_ledger_fee")
var normalFee: String? = null,
@Json(name = "median_fee")
var priorityFee: String? = null
)
// Rippled submit
@JsonClass(generateAdapter = true)
data class RippledSubmitResponse(
@Json(name = "result")
var result: RippledSubmitResult? = null
)
@JsonClass(generateAdapter = true)
data class RippledSubmitResult(
@Json(name = "engine_result_code")
var resultCode: Int? = null,
@Json(name = "engine_result_message")
var resultMessage: String? = null,
@Json(name = "error")
var error: String? = null,
@Json(name = "error_exception")
var errorException: String? = null
)

View file

@ -0,0 +1,111 @@
package com.tangem.blockchain.xrp.override;
import java.math.BigInteger;
public class XrpBase58 {
private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray();
private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long
private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS
private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1,
-1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1,
-1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
public static byte[] decodeBase58(String input) {
if (input == null) {
return null;
}
input = input.trim();
if (input.length() == 0) {
return new byte[0];
}
BigInteger resultNum = BigInteger.ZERO;
int nLeadingZeros = 0;
while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) {
nLeadingZeros++;
}
long acc = 0;
int nDigits = 0;
int p = nLeadingZeros;
while (p < input.length()) {
int v = BASE58_VALUES[input.charAt(p) & 0xff];
if (v >= 0) {
acc *= 58;
acc += v;
nDigits++;
if (nDigits == BASE58_CHUNK_DIGITS) {
resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc));
acc = 0;
nDigits = 0;
}
p++;
} else {
break;
}
}
if (nDigits > 0) {
long mul = 58;
while (--nDigits > 0) {
mul *= 58;
}
resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc));
}
final int BASE58_SPACE = -2;
while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) {
p++;
}
if (p < input.length()) {
return null;
}
byte[] plainNumber = resultNum.toByteArray();
int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0;
byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs];
System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs);
return result;
}
public static String encodeBase58(byte[] input) {
if (input == null) {
return null;
}
StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1);
BigInteger bn = new BigInteger(1, input);
long rem;
while (true) {
BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD);
bn = divideAndRemainder[0];
rem = divideAndRemainder[1].longValue();
if (bn.compareTo(BigInteger.ZERO) == 0) {
break;
}
for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) {
str.append(BASE58[(int) (rem % 58)]);
rem /= 58;
}
}
while (rem != 0) {
str.append(BASE58[(int) (rem % 58)]);
rem /= 58;
}
str.reverse();
int nLeadingZeros = 0;
while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) {
str.insert(0, BASE58[0]);
nLeadingZeros++;
}
return str.toString();
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.blockchain.xrp.override;
import com.ripple.core.types.known.tx.txns.Payment;
public class XrpPayment extends Payment {
public XrpPayment() {
super();
}
public XrpSignedTransaction prepare(byte[] pubKeyBytes) {
XrpSignedTransaction tx = XrpSignedTransaction.fromTx(this);
tx.prepare(pubKeyBytes);
return tx;
}
}

View file

@ -0,0 +1,81 @@
package com.tangem.blockchain.xrp.override;
import com.ripple.core.coretypes.Amount;
import com.ripple.core.coretypes.Blob;
import com.ripple.core.coretypes.STObject;
import com.ripple.core.coretypes.hash.HalfSha512;
import com.ripple.core.coretypes.hash.prefixes.HashPrefix;
import com.ripple.core.coretypes.uint.UInt32;
import com.ripple.core.serialized.BytesList;
import com.ripple.core.serialized.MultiSink;
import com.ripple.core.types.known.tx.Transaction;
import com.ripple.core.types.known.tx.signed.SignedTransaction;
import java.util.Arrays;
public class XrpSignedTransaction extends SignedTransaction {
private XrpSignedTransaction(Transaction of) {
txn = (Transaction) STObject.fromBytes(of.toBytes());
}
protected XrpSignedTransaction() {
}
public static XrpSignedTransaction fromTx(Transaction tx) {
return new XrpSignedTransaction(tx);
}
public void prepare(byte[] pubKeyBytes) {
prepare(pubKeyBytes, null, null, null);
}
public void prepare(byte[] pubKeyBytes,
Amount fee,
UInt32 Sequence,
UInt32 lastLedgerSequence) {
Blob pubKey = new Blob(pubKeyBytes);
// This won't always be specified
if (lastLedgerSequence != null) {
txn.put(UInt32.LastLedgerSequence, lastLedgerSequence);
}
if (Sequence != null) {
txn.put(UInt32.Sequence, Sequence);
}
if (fee != null) {
txn.put(Amount.Fee, fee);
}
txn.signingPubKey(pubKey);
if (Transaction.CANONICAL_FLAG_DEPLOYED) {
txn.setCanonicalSignatureFlag();
}
txn.checkFormat();
signingData = txn.signingData();
if (previousSigningData != null && Arrays.equals(signingData, previousSigningData)) {
return;
}
}
public void addSign(byte[] signature) {
try {
txn.txnSignature(new Blob(signature));
BytesList blob = new BytesList();
HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID);
txn.toBytesSink(new MultiSink(blob, id));
tx_blob = blob.bytesHex();
hash = id.finish();
} catch (Exception e) {
// electric paranoia
previousSigningData = null;
throw new RuntimeException(e);
} /*else {*/
previousSigningData = signingData;
// }
}
}

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Blockchain</string>
</resources>

View file

@ -0,0 +1,25 @@
package com.tangem.blockchain.bitcoin
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
import org.junit.jupiter.api.Assertions.*
class BitcoinAddressTest {
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "04752A727E14BBA5BD73B6714D72500F61FFD11026AD1196D2E1C54577CBEEAC3D11FC68A64700F8D533F4E311964EA8FB3AA26C588295F2133868D69C3E628693".hexToBytes()
val expected = "1D3vYSjCvzrsVVK5bNaPTjU3NxcN7NNXMN"
Truth.assertThat(BitcoinAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "1D3vYSjCvzrsVVK5bNaPTjU3NxcN7NNXMN"
Truth.assertThat(BitcoinAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.blockchain.common
import com.google.common.truth.Truth
import com.tangem.common.CardEnvironment
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.ethereum.EthereumWalletManager
import com.tangem.blockchain.stellar.StellarWalletManager
import com.tangem.blockchain.xrp.XrpWalletManager
import com.tangem.commands.ReadCommand
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class WalletManagerFactoryTest {
@Test
fun createBitcoinWalletManager() {
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(BitcoinWalletManager::class.java)
}
@Test
fun createEthereumWalletManager() {
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(EthereumWalletManager::class.java)
}
@Test
fun createStellarWalletManager() {
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(StellarWalletManager::class.java)
}
@Test
fun createCardanoWalletManager() {
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(CardanoWalletManager::class.java)
}
@Test
fun createXrpWalletManager() {
val data = "0108cb21000000002154200b534d4152542043415348000201028006322e31317200034104bdad63848f97c535da53cf8fd300d24fa33f0516d194aa78ec164a06994d00204bae243a424e316c6ec845e02d9b15eafae8c19018a926b0b7435e6e941cdadb0a0400007e210c5a81020028820407e30502830754414e47454d00840358525086400ed8734b877869722c7d0b37ffb154b9fef21c54bf2c6496feb1fb5c1fc28a2ac28e201dde84f27495fa7f08b3ca2be2fb4954bf0fe78af027d6cdc16c3eee923041048196aa4b410ac44a3b9cce18e7be226aea070acc83a9cf67540fac49af25129f6a538a28ad6341358e3c4f9963064f7e365372a651d374e5c23cdd37fd099bf2050a736563703235366b31000804000f4240070100090205dc604104d2b9fb288540d54e5b32ecaf0381cd571f97f6f1ecd036b66bb11aa52ffe9981110d883080e2e255c6b1640586f7765e6faa325d1340f49b56b83d9de56bc7ed6204000f42406304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
Truth.assertThat(walletManager)
.isInstanceOf(XrpWalletManager::class.java)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.blockchain.ethereum
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class EthereumAddressTest {
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "04BAEC8CD3BA50FDFE1E8CF2B04B58E17041245341CD1F1C6B3A496B48956DB4C896A6848BCF8FCFC33B88341507DD25E5F4609386C68086C74CF472B86E5C3820".hexToBytes()
val expected = "0xc63763572d45171e4c25ca0818b44e5dd7f5c15b"
Truth.assertThat(EthereumAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "0xc63763572d45171e4c25ca0818b44e5dd7f5c15b"
Truth.assertThat(EthereumAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.blockchain.stellar
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class StellarAddressTest {
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "EC5387D8B38BD9EF80BDBC78D0D7E1C53F08E269436C99D5B3C2DF4B2CE73012".hexToBytes()
val expected = "GDWFHB6YWOF5T34AXW6HRUGX4HCT6CHCNFBWZGOVWPBN6SZM44YBFUDZ"
Truth.assertThat(StellarAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "GDWFHB6YWOF5T34AXW6HRUGX4HCT6CHCNFBWZGOVWPBN6SZM44YBFUDZ"
Truth.assertThat(StellarAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.blockchain.xrp
import com.google.common.truth.Truth
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
internal class XrpAddressTest {
@Test
fun makeAddressFromCorrectSecpPublicKey() {
val walletPublicKey = "04D2B9FB288540D54E5B32ECAF0381CD571F97F6F1ECD036B66BB11AA52FFE9981110D883080E2E255C6B1640586F7765E6FAA325D1340F49B56B83D9DE56BC7ED".hexToBytes()
val expected = "rNxCXgKaCMAmowENKnYa5r8Ue78rjgrM6B"
Truth.assertThat(XrpAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun makeAddressFromCorrectEdPublicKey() {
val walletPublicKey = "12CC4DE73BACF875D7423D152E46C1A665F1718CBE7CA0FEB2BA28C149E11909".hexToBytes()
val expected = "rwWMNBs2GtJwfX7YNVV1sUYaPy6DRmDHB4"
Truth.assertThat(XrpAddressFactory.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "rwWMNBs2GtJwfX7YNVV1sUYaPy6DRmDHB4"
Truth.assertThat(XrpAddressValidator.validate(address))
.isTrue()
}
}

View file

@ -1,15 +1,15 @@
buildscript {
ext.kotlin_version = '1.3.61'
apply from: 'dependencies.gradle'
repositories {
google()
jcenter()
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
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'
classpath "com.android.tools.build:gradle:$versions.build_gradle"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "io.fabric.tools:gradle:1.31.0"
}
}

4
dependencies.gradle Normal file
View file

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

View file

@ -1,6 +1,6 @@
#Thu Aug 22 12:21:00 MSK 2019
#Thu Feb 27 13:33:42 AST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

4
jitpack.gradle Normal file
View file

@ -0,0 +1,4 @@
ext.jitpackSdk = [
group: 'com.github.Tangem',
version : '0.2.1',
]

View file

@ -1,6 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'com.github.dcendents.android-maven'
group='com.tangem'
@ -41,7 +40,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
}
repositories {
mavenCentral()

View file

@ -1 +1 @@
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-demo'
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-demo', ':blockchain'

View file

@ -1,5 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'com.github.dcendents.android-maven'
group='com.tangem'

View file

@ -1,31 +1,33 @@
apply plugin: "kotlin"
apply plugin: 'org.jetbrains.dokka'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
group = 'com.github.TangemCash'
version '0.1.0'
group = "$jitpackSdk.group"
version "$jitpackSdk.version"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.madgag.spongycastle:core:1.56.0.0"
implementation "com.madgag.spongycastle:prov:1.56.0.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.0"
implementation 'net.i2p.crypto:eddsa:0.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
}
sourceCompatibility = "8"
targetCompatibility = "8"
buildscript {
ext.kotlin_version = '1.3.50'
ext.dokka_version = '0.10.0'
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
}
}

View file

@ -1,6 +1,8 @@
package com.tangem
import com.tangem.commands.*
import com.tangem.common.CardEnvironment
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.*
import java.util.concurrent.Executors
@ -16,10 +18,12 @@ import java.util.concurrent.Executors
*/
class CardManager(
private val reader: CardReader,
private val cardManagerDelegate: CardManagerDelegate? = null) {
private val cardManagerDelegate: CardManagerDelegate? = null,
private val config: Config = Config()
) {
private var terminalKeysService: TerminalKeysService? = null
private var isBusy = false
private val cardEnvironmentRepository = mutableMapOf<String, CardEnvironment>()
private val cardManagerExecutor = Executors.newSingleThreadExecutor()
init {
@ -67,12 +71,13 @@ class CardManager(
callback: (result: TaskEvent<SignResponse>) -> Unit) {
val signCommand: SignCommand
try {
signCommand = SignCommand(hashes, cardId)
signCommand = SignCommand(hashes)
} catch (error: Exception) {
if (error is TaskError) {
callback(TaskEvent.Completion(error))
} else {
callback(TaskEvent.Completion(TaskError.GenericError(error.message)))
Log.e(this::class.simpleName!!, error.message ?: "")
callback(TaskEvent.Completion(TaskError.UnknownError()))
}
return
}
@ -91,8 +96,23 @@ class CardManager(
*/
fun readIssuerData(cardId: String,
callback: (result: TaskEvent<ReadIssuerDataResponse>) -> Unit) {
val getIssuerDataCommand = ReadIssuerDataCommand(cardId)
val task = SingleCommandTask(getIssuerDataCommand)
val task = ReadIssuerDataTask(config.issuerPublicKey)
runTask(task, cardId, callback)
}
/**
* This task retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerExtraDataTask],
* provides card response in the form of [ReadIssuerExtraDataResponse].
*/
fun readIssuerExtraData(cardId: String,
callback: (result: TaskEvent<ReadIssuerExtraDataResponse>) -> Unit) {
val task = ReadIssuerExtraDataTask(config.issuerPublicKey)
runTask(task, cardId, callback)
}
@ -103,7 +123,7 @@ class CardManager(
* wallet balance signed by the issuer or additional issuers attestation data.
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key.
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand],
* provides card response in the form of [WriteIssuerDataResponse].
@ -113,15 +133,89 @@ class CardManager(
issuerDataSignature: ByteArray,
issuerDataCounter: Int? = null,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
val writeIssuerDataCommand = WriteIssuerDataCommand(
cardId,
issuerData,
issuerDataSignature,
issuerDataCounter)
val task = SingleCommandTask(writeIssuerDataCommand)
val task = WriteIssuerDataTask(
issuerData,
issuerDataSignature,
issuerDataCounter,
config.issuerPublicKey
)
runTask(task, cardId, callback)
}
/**
* This task writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed
* to write entire Issuer_Extra_Data.
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand],
* provides card response in the form of [WriteIssuerDataResponse].
*/
fun writeIssuerExtraData(cardId: String,
issuerData: ByteArray,
startingSignature: ByteArray,
finalizingSignature: ByteArray,
issuerDataCounter: Int? = null,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
val task = WriteIssuerExtraDataTask(
issuerData,
startingSignature, finalizingSignature,
config.issuerPublicKey,
issuerDataCounter
)
runTask(task, cardId, callback)
}
/**
* This command write some of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of User_Counter and User_Data protected only by PIN1.
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
fun writeUserData(
cardId: String,
userData: ByteArray? = null,
userProtectedData: ByteArray? = null,
userCounter: Int? = null,
userProtectedCounter: Int? = null,
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
) {
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
val task = SingleCommandTask(writeUserDataCommand)
runTask(task, cardId, callback)
}
/**
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*/
fun readUserData(cardId: String, callback: (result: TaskEvent<ReadUserDataResponse>) -> Unit) {
val task = SingleCommandTask(ReadUserDataCommand())
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.
@ -134,7 +228,7 @@ class CardManager(
*/
fun createWallet(cardId: String,
callback: (result: TaskEvent<CreateWalletResponse>) -> Unit) {
val createWalletCommand = CreateWalletCommand(cardId)
val createWalletCommand = CreateWalletCommand()
val task = SingleCommandTask(createWalletCommand)
runTask(task, cardId, callback)
}
@ -148,7 +242,7 @@ class CardManager(
*/
fun purgeWallet(cardId: String,
callback: (result: TaskEvent<PurgeWalletResponse>) -> Unit) {
val purgeWalletCommand = PurgeWalletCommand(cardId)
val purgeWalletCommand = PurgeWalletCommand()
val task = SingleCommandTask(purgeWalletCommand)
runTask(task, cardId, callback)
}
@ -156,14 +250,13 @@ class CardManager(
/**
*/
fun <T> runTask(task: Task<T>, cardId: String? = null,
callback: (result: TaskEvent<T>) -> Unit) {
fun <T> runTask(task: Task<T>, cardId: String? = null, callback: (result: TaskEvent<T>) -> Unit) {
if (isBusy) {
callback(TaskEvent.Completion(TaskError.Busy()))
return
}
val environment = fetchCardEnvironment(cardId)
val environment = prepareCardEnvironment(cardId)
isBusy = true
task.reader = reader
@ -187,7 +280,21 @@ class CardManager(
runTask(task, cardId, callback)
}
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId)
/**
* Allows to set a particular [TerminalKeysService] to retrieve terminal keys.
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
*/
fun setTerminalKeysService(terminalKeysService: TerminalKeysService) {
this.terminalKeysService = terminalKeysService
}
private fun prepareCardEnvironment(cardId: String?): CardEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
return CardEnvironment(
cardId = cardId,
terminalKeys = terminalKeys
)
}
companion object
}

View file

@ -13,13 +13,19 @@ interface CardManagerDelegate {
/**
* It is called when user is expected to scan a Tangem Card with an Android device.
*/
fun onNfcSessionStarted()
fun onNfcSessionStarted(cardId: String?)
/**
* It is called when security delay is triggered by the card.
* A user is expected to hold the card until the security delay is over.
*/
fun onSecurityDelay(ms: Int)
fun onSecurityDelay(ms: Int, totalDurationSeconds: Int)
/**
* It is called when long tasks are performed.
* A user is expected to hold the card until the task is complete.
*/
fun onDelay(total: Int, current: Int, step: Int)
/**
* It is called when user takes the card away from the Android device during the scanning
@ -35,7 +41,7 @@ interface CardManagerDelegate {
/**
* It is called when some error occur during NFC session.
*/
fun onError(error: TaskError? = null)
fun onError(error: TaskError)
/**
* It is called when a user is expected to enter pin code.

View file

@ -0,0 +1,6 @@
package com.tangem
class Config(
val linkedTerminal: Boolean = true,
val issuerPublicKey: ByteArray? = null
)

View file

@ -1,14 +1,16 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.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.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.TaskError
/**
@ -22,7 +24,16 @@ class CheckWalletResponse(
val cardId: String,
val salt: ByteArray,
val walletSignature: ByteArray
) : CommandResponse
) : CommandResponse {
fun verify(curve: EllipticCurve, publicKey: ByteArray, challenge: ByteArray): Boolean {
return CryptoUtils.verify(
publicKey,
challenge + salt,
walletSignature,
curve)
}
}
/**
* This command proves that the wallet private key from the card corresponds to the wallet public key.
@ -32,18 +43,16 @@ class CheckWalletResponse(
* @property cardId Unique Tangem card ID number
* @property challenge Random challenge generated by application
*/
class CheckWalletCommand(
private val cardId: String,
private val challenge: ByteArray
) : CommandSerializer<CheckWalletResponse>() {
class CheckWalletCommand : CommandSerializer<CheckWalletResponse>() {
val challenge = CryptoUtils.generateRandomBytes(16)
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = listOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Challenge, challenge)
)
return CommandApdu(Instruction.CheckWallet, tlvData)
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? {

View file

@ -1,6 +1,6 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.toInt

Some files were not shown because too many files have changed in this diff Show more