Updated on 2026-08-14
This commit is contained in:
commit
660cab44c8
161 changed files with 8909 additions and 630 deletions
32
.gitignore
vendored
32
.gitignore
vendored
|
|
@ -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
1
.idea/gradle.xml
generated
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
1
blockchain/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
78
blockchain/build.gradle
Normal file
78
blockchain/build.gradle
Normal 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'
|
||||
}
|
||||
BIN
blockchain/libs/ripple-core-0.0.1.jar
Normal file
BIN
blockchain/libs/ripple-core-0.0.1.jar
Normal file
Binary file not shown.
21
blockchain/proguard-rules.pro
vendored
Normal file
21
blockchain/proguard-rules.pro
vendored
Normal 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
|
||||
1
blockchain/src/main/AndroidManifest.xml
Normal file
1
blockchain/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<manifest package="com.tangem.blockchain" />
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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/"
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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>?
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
//)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
3
blockchain/src/main/res/values/strings.xml
Normal file
3
blockchain/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Blockchain</string>
|
||||
</resources>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
10
build.gradle
10
build.gradle
|
|
@ -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
4
dependencies.gradle
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ext.versions = [
|
||||
kotlin : '1.3.61',
|
||||
build_gradle: '3.6.0',
|
||||
]
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
4
jitpack.gradle
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ext.jitpackSdk = [
|
||||
group: 'com.github.Tangem',
|
||||
version : '0.2.1',
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
apply plugin: 'java-library'
|
||||
apply plugin: 'com.github.dcendents.android-maven'
|
||||
|
||||
group='com.tangem'
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 issuer’s 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 issuer’s attestation data.
|
||||
* @param cardId CID, Unique Tangem card ID number.
|
||||
* @param issuerData Data provided by issuer.
|
||||
* @param issuerDataSignature Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* @param issuerDataSignature Issuer’s 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 issuer’s 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 Issuer’s 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 Issuer’s 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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
6
tangem-core/src/main/java/com/tangem/Config.kt
Normal file
6
tangem-core/src/main/java/com/tangem/Config.kt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem
|
||||
|
||||
class Config(
|
||||
val linkedTerminal: Boolean = true,
|
||||
val issuerPublicKey: ByteArray? = null
|
||||
)
|
||||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue