Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-28 08:30:37 +00:00
commit 05a9671ed1
9 changed files with 195 additions and 156 deletions

View file

@ -0,0 +1,25 @@
package com.tangem.data.network;
import com.tangem.data.network.model.BitcoreBalance;
import com.tangem.data.network.model.BitcoreSendBody;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
import java.util.List;
import io.reactivex.Single;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
public interface DucatusApi {
@GET(Server.ApiDucatus.Method.BALANCE)
Single<BitcoreBalance> ducatusBalance(@Path("address") String address);
@GET(Server.ApiDucatus.Method.UTXO)
Single<List<BitcoreUtxo>> ducatusUnspents(@Path("address") String address);
@POST(Server.ApiDucatus.Method.SEND)
Single<BitcoreSendResponse> ducatusSend(@Body BitcoreSendBody body);
}

View file

@ -129,4 +129,14 @@ public class Server {
static final String PUSH = URL_BLOCKCHAININFO + "pushtx";
}
}
public static class ApiDucatus {
public static final String URL_DUCATUS = ServerURL.API_DUCATUS + "api/DUC/mainnet/";
public static class Method {
static final String BALANCE = URL_DUCATUS + "address/{address}/balance";
static final String UTXO = URL_DUCATUS + "address/{address}/?unspent=true";
static final String SEND = URL_DUCATUS + "tx/send";
}
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.data.network;
import com.tangem.App;
import com.tangem.data.network.model.BitcoreBalance;
import com.tangem.data.network.model.BitcoreBalanceAndUnspents;
import com.tangem.data.network.model.BitcoreSendBody;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
import com.tangem.tangem_card.util.Log;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class ServerApiBitcore {
private static String TAG = ServerApiBitcore.class.getSimpleName();
public void getBalanceAndUnspents(String wallet, SingleObserver<BitcoreBalanceAndUnspents> balanceAndUnspentsObserver) {
Log.i(TAG, "new getAddressAndUnspents request");
DucatusApi api = App.Companion.getNetworkComponent().getRetrofitDucatus().create(DucatusApi.class);
Single<BitcoreBalance> balanceObservable = api.ducatusBalance(wallet);
Single<List<BitcoreUtxo>> unspentsObservable = api.ducatusUnspents(wallet)
.onErrorReturnItem(new ArrayList<>());
Single.zip(balanceObservable, unspentsObservable, BitcoreBalanceAndUnspents::new)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(balanceAndUnspentsObserver);
}
public void sendTransaction(String tx, SingleObserver<BitcoreSendResponse> sendObserver) {
Log.i(TAG, "new getAddress request");
DucatusApi api = App.Companion.getNetworkComponent().getRetrofitDucatus().create(DucatusApi.class);
Single<BitcoreSendResponse> sendObservable = api.ducatusSend(new BitcoreSendBody(tx))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
sendObservable.subscribe(sendObserver);
}
}

View file

@ -16,4 +16,5 @@ class ServerURL {
static final String API_STELLAR_RESERVE = "https://horizon.sui.li/";
static final String API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/";
static final String API_BLOCKCHAIN_INFO = "https://blockchain.info/";
static final String API_DUCATUS = "https://ducapi.rocknblock.io/";
}

View file

@ -0,0 +1,34 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class BitcoreBalance(
@SerializedName("confirmed")
var confirmed: Long? = null,
@SerializedName("unconfirmed")
var unconfirmed: Long? = null
)
data class BitcoreUtxo(
@SerializedName("mintTxid")
var mintTxid: String? = null,
@SerializedName("mintIndex")
var mintIndex: Int? = null,
@SerializedName("value")
var value: Long? = null,
@SerializedName("script")
var script: String? = null
)
data class BitcoreBalanceAndUnspents(
var balance: BitcoreBalance,
var unspents: List<BitcoreUtxo>
)
data class BitcoreSendResponse(
var txid: String? = null
)

View file

@ -0,0 +1,14 @@
package com.tangem.data.network.model;
import java.util.ArrayList;
import java.util.List;
public class BitcoreSendBody {
private List<String> rawTx;
public BitcoreSendBody(String tx) {
List<String> txList = new ArrayList<>();
txList.add(tx);
rawTx = txList;
}
}

View file

@ -38,6 +38,9 @@ interface NetworkComponent {
@get:Named(Server.ApiBlockchainInfo.URL_BLOCKCHAININFO)
val retrofitBlockchainInfo: Retrofit
@get:Named(Server.ApiDucatus.URL_DUCATUS)
val retrofitDucatus: Retrofit
@get:Named("socket")
val socket: Socket

View file

@ -133,6 +133,20 @@ internal class NetworkModule {
return builder.build()
}
@Singleton
@Provides
@Named(Server.ApiDucatus.URL_DUCATUS)
fun provideRetrofitDucatus(): Retrofit {
val builder = Retrofit.Builder()
.baseUrl(Server.ApiDucatus.URL_DUCATUS)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
if (BuildConfig.DEBUG)
builder.client(createOkHttpClient())
return builder.build()
}
private fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(createHttpLoggingInterceptor()).build()
}

View file

@ -5,7 +5,11 @@ import android.text.InputFilter;
import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiBitcore;
import com.tangem.data.network.ServerApiInsight;
import com.tangem.data.network.model.BitcoreBalanceAndUnspents;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import com.tangem.tangem_card.data.TangemCard;
@ -37,6 +41,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class DucatusEngine extends BtcEngine {
private static final String TAG = DucatusEngine.class.getSimpleName();
public BtcData coinData = null;
@ -169,7 +176,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public Uri getWalletExplorerUri() {
return Uri.parse("https://insight.ducatus.io/insight/address/" + ctx.getCoinData().getWallet());
return Uri.parse("https://insight.ducatus.io/#/DUC/mainnet/address/" + ctx.getCoinData().getWallet());
}
@Override
@ -397,13 +404,6 @@ public class DucatusEngine extends BtcEngine {
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
// // Build script for our address
// List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
// byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
//
// // Collect unspent
// unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) {
unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null));
}
@ -498,70 +498,43 @@ public class DucatusEngine extends BtcEngine {
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiInsight serverApiInsight = new ServerApiInsight();
ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
SingleObserver<BitcoreBalanceAndUnspents> balanceAndUnspentsObserver = new DisposableSingleObserver<BitcoreBalanceAndUnspents>() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
public void onSuccess(BitcoreBalanceAndUnspents balanceAndUnspents) {
try {
String walletAddress = insightResponse.getAddrStr();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
coinData.setBalanceConfirmed(balanceAndUnspents.getBalance().getConfirmed());
coinData.setBalanceUnconfirmed(balanceAndUnspents.getBalance().getUnconfirmed());
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(insightResponse.getBalanceSat());
coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat());
coinData.setValidationNodeDescription(ServerApiInsight.lastNode);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception");
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
public void onSuccess(String method, List<InsightUtxo> utxoList) {
// case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method
try {
coinData.getUnspentTransactions().clear();
for (InsightUtxo utxo : utxoList) {
for (BitcoreUtxo utxo : balanceAndUnspents.getUnspents()) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTxid();
trUnspent.amount = utxo.getSatoshis();
trUnspent.outputN = utxo.getVout();
trUnspent.script = utxo.getScriptPubKey();
trUnspent.txID = utxo.getMintTxid();
trUnspent.amount = utxo.getValue();
trUnspent.outputN = utxo.getMintIndex();
trUnspent.script = utxo.getScript();
coinData.getUnspentTransactions().add(trUnspent);
}
blockchainRequestsCallbacks.onComplete(true);
} catch (Exception e) {
Log.e(TAG, "FAIL BITCORE_BALANCE_AND_UNSPENTS Exception");
e.printStackTrace();
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) { //TODO: rework request sequence
ctx.setError(message);
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "FAIL BITCORE_BALANCE_AND_UNSPENTS Exception");
e.printStackTrace();
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_ADDRESS, coinData.getWallet(), "");
serverApiInsight.requestData(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS, coinData.getWallet(), "");
ServerApiBitcore serverApiBitcore = new ServerApiBitcore();
serverApiBitcore.getBalanceAndUnspents(coinData.getWallet(), balanceAndUnspentsObserver);
}
// private final static BigDecimal relayFee = new BigDecimal(0.00001);
@ -570,65 +543,6 @@ public class DucatusEngine extends BtcEngine {
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
// coinData.minFee=null;
// coinData.maxFee=null;
// coinData.normalFee=null;
//
// final ServerApiInsight serverApiInsight = new ServerApiInsight();
//
// final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
// @Override
// public void onSuccess(String method, InsightResponse insightResponse) {
// if ( method.equals(ServerApiInsight.INSIGHT_FEE)) {
// try {
// BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB
// BigDecimal normalFee = new BigDecimal(insightResponse.getFee3());
// BigDecimal maxFee = new BigDecimal(insightResponse.getFee6());
//
// if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) {
// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "","");
// }
//
// minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size
// normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
// maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
//
//// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network
//// if (fee.compareTo(relayFee) < 0) {
//// fee = relayFee;
//// }
// minFee = minFee.setScale(8, RoundingMode.DOWN);
// normalFee = normalFee.setScale(8, RoundingMode.DOWN);
// maxFee = maxFee.setScale(8, RoundingMode.DOWN);
//
// coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency());
// coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency());
// coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency());
//
// blockchainRequestsCallbacks.onComplete(true);
//
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
// }
//
// @Override
// public void onSuccess (String method, List<InsightResponse> utxoList) {
// Log.e(TAG, "Wrong response body, InsightResponse expected");
// }
//
// @Override
// public void onFail(String method, String message) {
// if (!serverApiInsight.isRequestsSequenceCompleted()) {
// ctx.setError(message);
// blockchainRequestsCallbacks.onComplete(false);
// }
// }
// };
// serverApiInsight.setResponseListener(responseListener);
//
// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", ""); TODO: fee api returns -1 now
coinData.minFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000089)), ctx.getBlockchain().getCurrency()); //fee for byte from Ducatus wallet for android
coinData.normalFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000144)), ctx.getBlockchain().getCurrency());
@ -639,51 +553,28 @@ public class DucatusEngine extends BtcEngine {
@Override
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) {
final ServerApiInsight serverApiInsight = new ServerApiInsight();
final String txStr = BTCUtils.toHex(txForSend);
final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
SingleObserver<BitcoreSendResponse> sendResponseObserver = new DisposableSingleObserver<BitcoreSendResponse>() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
if (method.equals(ServerApiInsight.INSIGHT_SEND)) {
String resultString = insightResponse.toString();
try {
if (resultString.isEmpty()) {
ctx.setError("No response from node");
blockchainRequestsCallbacks.onComplete(false);
} else { // TODO: Make check for a valid send response
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}
} catch (Exception e) {
if (e.getMessage() != null) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
} else {
ctx.setError(e.getClass().getName());
blockchainRequestsCallbacks.onComplete(false);
Log.e(TAG, resultString);
}
}
}
}
@Override
public void onSuccess(String method, List<InsightUtxo> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) {
ctx.setError(message);
public void onSuccess(BitcoreSendResponse sendResponse) {
if (sendResponse.getTxid() != null) {
blockchainRequestsCallbacks.onComplete(true);
} else {
ctx.setError("Unknown send error");
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_SEND, "", txStr);
@Override
public void onError(Throwable e) {
Log.e(TAG, "onError: Bitcore sendTransaction" + e.getMessage());
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
ServerApiBitcore serverApiBitcore = new ServerApiBitcore();
serverApiBitcore.sendTransaction(txStr, sendResponseObserver);
}
}