Updated on 2026-08-14
This commit is contained in:
parent
22d155e526
commit
b1134eb19e
17 changed files with 3029 additions and 7 deletions
327
app/src/main/java/com/tangem/data/network/Cryptonit.java
Normal file
327
app/src/main/java/com/tangem/data/network/Cryptonit.java
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
package com.tangem.data.network;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.internal.LinkedTreeMap;
|
||||
import com.jakewharton.retrofit2.adapter.rxjava2.HttpException;
|
||||
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||
import com.tangem.util.Util;
|
||||
import com.tangem.wallet.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
//import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Part;
|
||||
|
||||
/**
|
||||
* HTTP
|
||||
* Used in Cryptonit service
|
||||
*/
|
||||
public class Cryptonit {
|
||||
private static final String SERVER_URL = "https://www.cryptonit.net:443/gateway/";
|
||||
|
||||
private static class Method {
|
||||
public static final String AUTHENTICATE = SERVER_URL + "public/authenticate";
|
||||
public static final String BALANCE = SERVER_URL + "private/balance";
|
||||
public static final String WITHDRAW_COINS = SERVER_URL + "private/withdrawCoins";
|
||||
}
|
||||
|
||||
public static class Model {
|
||||
|
||||
static class Authenticate
|
||||
{
|
||||
public static class Request
|
||||
{
|
||||
public String username;
|
||||
public String password;
|
||||
}
|
||||
public static class Response
|
||||
{
|
||||
boolean success;
|
||||
String[] missing_authenticators;
|
||||
public Object[] infos;
|
||||
public Object[] warnings;
|
||||
public Object[] errors;
|
||||
AuthenticationResult results;
|
||||
}
|
||||
|
||||
static class AuthenticationResult {
|
||||
// token (string): authentication token,
|
||||
// nick (string),
|
||||
// stayLoggedIn (boolean),
|
||||
// integer lastLogin (integer): JavaScript time OR NEVER for first login,
|
||||
// preferredLanguage (string) = ['en' or 'de']
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Balance {
|
||||
static class Request {
|
||||
String[] currencies;
|
||||
}
|
||||
public static class Response {
|
||||
public boolean success;
|
||||
public Object[] infos;
|
||||
public Object[] warnings;
|
||||
public Object[] errors;
|
||||
public BalanceResult[] results;
|
||||
|
||||
public static class BalanceResult {
|
||||
public String currency;
|
||||
public double balance;
|
||||
public String receiveAddress; //the current address to receive funds for this account, if available,
|
||||
public boolean fiat; // Is this a FIAT currency? If false, this is a CRYPTO currency.,
|
||||
public Object[] unprocessedTransactions; // (array, optional): list of unprocessed transactions, if pass field withTransactions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WithdrawCoins {
|
||||
public static class Request {
|
||||
public String currency;
|
||||
public Double amount;
|
||||
String toAddress;
|
||||
Double includeMinerFee;
|
||||
String password;
|
||||
}
|
||||
public static class Response {
|
||||
public Boolean success;
|
||||
public String[] missing_authenticators;
|
||||
public Object[] infos;
|
||||
public Object[] warnings;
|
||||
public Object[] errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Api {
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST(Method.AUTHENTICATE)
|
||||
Call<Model.Authenticate.Response> authenticate(@Body Model.Authenticate.Request request);
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST(Method.BALANCE)
|
||||
Observable<Model.Balance.Response> getBalance(@Header("Auth-Token") String authToken, @Body Model.Balance.Request request);
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST(Method.WITHDRAW_COINS)
|
||||
Observable<Model.WithdrawCoins.Response> withdrawCoins(@Header("Auth-Token") String authToken, @Body Model.WithdrawCoins.Request request);
|
||||
}
|
||||
|
||||
private Api api = null;
|
||||
private BalanceListener balanceListener;
|
||||
private WithdrawalListener withdrawalListener;
|
||||
private ErrorListener errorListener;
|
||||
private Context context;
|
||||
private String authToken;
|
||||
|
||||
public Cryptonit(Context context) {
|
||||
this.context = context;
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
username = sp.getString(context.getResources().getString(R.string.key_cryptonit_username), "");
|
||||
password = sp.getString(context.getResources().getString(R.string.key_cryptonit_password), "");
|
||||
fee = sp.getString(context.getResources().getString(R.string.key_cryptonit_fee), "0.0");
|
||||
}
|
||||
|
||||
public String username;
|
||||
public String password;
|
||||
private String fee;
|
||||
|
||||
public String getFee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
public void setFee(String value)
|
||||
{
|
||||
fee=value;
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
sp.edit()
|
||||
.putString(context.getResources().getString(R.string.key_cryptonit_fee), fee)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public Boolean haveAccountInfo() {
|
||||
return !username.isEmpty() && !password.isEmpty();
|
||||
}
|
||||
|
||||
public void saveAccountInfo() {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
sp.edit()
|
||||
.putString(context.getResources().getString(R.string.key_cryptonit_username), username)
|
||||
.putString(context.getResources().getString(R.string.key_cryptonit_password), password)
|
||||
.apply();
|
||||
|
||||
}
|
||||
|
||||
public interface BalanceListener {
|
||||
void onBalanceData(Model.Balance.Response response);
|
||||
}
|
||||
|
||||
public interface WithdrawalListener {
|
||||
void onWithdrawalComplete(Model.WithdrawCoins.Response response);
|
||||
}
|
||||
|
||||
public interface ErrorListener {
|
||||
void onError(Throwable throwable);
|
||||
}
|
||||
|
||||
public void setBalanceListener(BalanceListener listener) {
|
||||
balanceListener = listener;
|
||||
}
|
||||
|
||||
public void setWithdrawalListener(WithdrawalListener listener) {
|
||||
withdrawalListener = listener;
|
||||
}
|
||||
|
||||
public void setErrorListener(ErrorListener listener) {
|
||||
errorListener = listener;
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestBalance(String currency) {
|
||||
|
||||
initApi();
|
||||
|
||||
// Log.e("CRYPTONIT2", "username: " + username);
|
||||
// Log.e("CRYPTONIT2", "password: " + password);
|
||||
// Log.e("CRYPTONIT2", "auth-token: " + authToken);
|
||||
|
||||
Model.Balance.Request request=new Model.Balance.Request();
|
||||
request.currencies=new String[] {currency};
|
||||
api.getBalance(authToken, request)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(balanceModel -> balanceListener.onBalanceData(balanceModel),
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestWithdrawCoins(String currency, Double amount, String address) {
|
||||
|
||||
initApi();
|
||||
|
||||
// Log.e("CRYPTONIT2", "username: " + username);
|
||||
// Log.e("CRYPTONIT2", "password: " + password);
|
||||
// Log.e("CRYPTONIT2", "auth-token: " + authToken);
|
||||
|
||||
Model.WithdrawCoins.Request request=new Model.WithdrawCoins.Request();
|
||||
request.currency=currency;
|
||||
request.amount=amount;
|
||||
request.toAddress=address;
|
||||
request.includeMinerFee=Double.parseDouble(fee);
|
||||
request.password=password;
|
||||
|
||||
api.withdrawCoins(authToken, request)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> {
|
||||
if (response.success != null && response.success)
|
||||
withdrawalListener.onWithdrawalComplete(response);
|
||||
else {
|
||||
errorListener.onError(new Exception(((LinkedTreeMap) response.errors[0]).entrySet().toArray()[0].toString()));
|
||||
}
|
||||
},
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
private void FireError(Throwable e) throws IOException {
|
||||
if (e.getClass() == HttpException.class && ((HttpException) e).code() == 500) {
|
||||
JsonObject jsonObject = new JsonParser().parse(((HttpException) e).response().errorBody().string()).getAsJsonObject();
|
||||
errorListener.onError(new Exception(e.getMessage() + ": " + jsonObject.get("errors").getAsString()));
|
||||
// if (jsonObject.get("reason").getAsString().equals("Invalid nonce")) nonce += 1000;
|
||||
} else {
|
||||
errorListener.onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initApi() {
|
||||
|
||||
if (api != null) return;
|
||||
|
||||
// HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||||
// logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient httpClient = new OkHttpClient.Builder().
|
||||
// addInterceptor(logging).
|
||||
addInterceptor(new AuthorizationInterceptor()).build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(SERVER_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(httpClient)
|
||||
.build();
|
||||
|
||||
api = retrofit.create(Api.class);
|
||||
}
|
||||
|
||||
public class AuthorizationInterceptor implements Interceptor {
|
||||
|
||||
AuthorizationInterceptor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request mainRequest=chain.request();
|
||||
|
||||
if( !mainRequest.url().toString().endsWith("authenticate")&& authToken==null )
|
||||
{
|
||||
Model.Authenticate.Request authRequest=new Model.Authenticate.Request();
|
||||
authRequest.username=username;
|
||||
authRequest.password=password;
|
||||
retrofit2.Response<Model.Authenticate.Response> authResponse=api.authenticate(authRequest).execute();
|
||||
if( authResponse.isSuccessful() && authResponse.body().success)
|
||||
{
|
||||
String newToken = authResponse.headers().get("auth-token");
|
||||
if (newToken != null) {
|
||||
authToken = newToken;
|
||||
}
|
||||
}else{
|
||||
throw new IOException("Authentication error: "+authResponse.message());
|
||||
}
|
||||
}
|
||||
|
||||
if( authToken!=null ) {
|
||||
mainRequest = mainRequest.newBuilder().addHeader("auth-token", authToken).build();
|
||||
}
|
||||
Response mainResponse = chain.proceed(mainRequest);
|
||||
if (!mainResponse.isSuccessful()) {
|
||||
authToken=null;
|
||||
}
|
||||
return mainResponse;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
package com.tangem.data.network;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.internal.LinkedTreeMap;
|
||||
import com.jakewharton.retrofit2.adapter.rxjava2.HttpException;
|
||||
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||
import com.tangem.util.Util;
|
||||
import com.tangem.wallet.R;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
//import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.Path;
|
||||
|
||||
/**
|
||||
* HTTP
|
||||
* Used in Cryptonit_OtherAPI service
|
||||
*/
|
||||
public class Cryptonit_OtherAPI {
|
||||
private static final String SERVER_URL = "https://api.cryptonit.net/api/";
|
||||
|
||||
private static class Method {
|
||||
public static final String BALANCE = SERVER_URL + "balance/{cryptoCurrency}%2F{fiatCurrency}";
|
||||
public static final String CRYPTO_WITHDRAWAL = SERVER_URL + "crypto_withdrawal/";
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
public static class Balance{
|
||||
@SerializedName("btc_balance")
|
||||
public String btc_balance;
|
||||
@SerializedName("eur_balance")
|
||||
public String eur_balance;
|
||||
@SerializedName("eth_balance")
|
||||
public String eth_balance;
|
||||
@SerializedName("etn_balance")
|
||||
public String etn_balance;
|
||||
@SerializedName("btc_reserved")
|
||||
public String btc_reserved;
|
||||
@SerializedName("eur_reserved")
|
||||
public String eur_reserved;
|
||||
@SerializedName("eth_reserved")
|
||||
public String eth_reserved;
|
||||
@SerializedName("etn_reserved")
|
||||
public String etn_reserved;
|
||||
@SerializedName("btc_available")
|
||||
public String btc_available;
|
||||
@SerializedName("eur_available")
|
||||
public String eur_available;
|
||||
@SerializedName("eth_available")
|
||||
public String eth_available;
|
||||
@SerializedName("etn_available")
|
||||
public String etn_available;
|
||||
@SerializedName("btceur_fee")
|
||||
public String btceur_fee;
|
||||
@SerializedName("etheur_fee")
|
||||
public String etheur_fee;
|
||||
@SerializedName("etnbtc_fee")
|
||||
public String etnbtc_fee;
|
||||
@SerializedName("fee")
|
||||
public String fee;
|
||||
}
|
||||
|
||||
public static class CryptoWithdrawal {
|
||||
@SerializedName("success")
|
||||
public Boolean success;
|
||||
@SerializedName("status")
|
||||
public String status;
|
||||
@SerializedName("reason")
|
||||
public Object reason;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Api {
|
||||
@Multipart
|
||||
@Headers("accept: multipart/form-data")
|
||||
@POST(Method.BALANCE)
|
||||
Observable<Response.Balance> getBalance(
|
||||
@Path("cryptoCurrency") String cryptoCurrency, @Path("fiatCurrency") String fiatCurrency,
|
||||
@Part(value = "key") RequestBody key, @Part("signature") RequestBody signature, @Part("nonce") RequestBody nonce);
|
||||
|
||||
@Multipart
|
||||
@Headers("accept: multipart/form-data")
|
||||
@POST(Method.CRYPTO_WITHDRAWAL)
|
||||
Observable<Response.CryptoWithdrawal> cryptoWithdrawal(
|
||||
@Part(value = "currency") RequestBody currency, @Part("amount") RequestBody amount, @Part("address") RequestBody address,
|
||||
@Part(value = "key") RequestBody key, @Part("signature") RequestBody signature, @Part("nonce") RequestBody nonce);
|
||||
}
|
||||
|
||||
private Api api = null;
|
||||
private BalanceListener balanceListener;
|
||||
private WithdrawalListener withdrawalListener;
|
||||
private ErrorListener errorListener;
|
||||
private Context context;
|
||||
|
||||
public Cryptonit_OtherAPI(Context context) {
|
||||
this.context = context;
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
key = sp.getString(context.getResources().getString(R.string.key_cryptonit_key), "");
|
||||
userId = sp.getString(context.getResources().getString(R.string.key_cryptonit_user_id), "");
|
||||
secret = sp.getString(context.getResources().getString(R.string.key_cryptonit_secret), "");
|
||||
nonce = sp.getInt(context.getResources().getString(R.string.key_cryptonit_nonce), 0);
|
||||
}
|
||||
|
||||
public String key;
|
||||
public String userId;
|
||||
public String secret;
|
||||
private Integer nonce;
|
||||
|
||||
public String getSecretDescription() {
|
||||
if (secret == null || secret.isEmpty()) return "";
|
||||
return secret.substring(0, 3) + "..." + secret.substring(secret.length() - 3, secret.length());
|
||||
}
|
||||
|
||||
public Boolean havaAccountInfo() {
|
||||
return (!userId.isEmpty() && !key.isEmpty() && !secret.isEmpty());
|
||||
}
|
||||
|
||||
public void saveAccountInfo() {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
nonce = 1;
|
||||
sp.edit()
|
||||
.putString(context.getResources().getString(R.string.key_cryptonit_key), key)
|
||||
.putString(context.getResources().getString(R.string.key_cryptonit_user_id), userId)
|
||||
.putString(context.getResources().getString(R.string.key_cryptonit_secret), secret)
|
||||
.putInt(context.getResources().getString(R.string.key_cryptonit_nonce), nonce)
|
||||
.apply();
|
||||
|
||||
}
|
||||
|
||||
private void incNonce() {
|
||||
nonce++;
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
sp.edit().putInt(context.getResources().getString(R.string.key_cryptonit_nonce), nonce).apply();
|
||||
}
|
||||
|
||||
public interface BalanceListener {
|
||||
void onBalanceData(Response.Balance response);
|
||||
}
|
||||
|
||||
public interface WithdrawalListener {
|
||||
void onWithdrawalComplete(Response.CryptoWithdrawal response);
|
||||
}
|
||||
|
||||
public interface ErrorListener {
|
||||
void onError(Throwable throwable);
|
||||
}
|
||||
|
||||
public void setBalanceListener(BalanceListener listener) {
|
||||
balanceListener = listener;
|
||||
}
|
||||
|
||||
public void setWithdrawalListener(WithdrawalListener listener) {
|
||||
withdrawalListener = listener;
|
||||
}
|
||||
|
||||
public void setErrorListener(ErrorListener listener) {
|
||||
errorListener = listener;
|
||||
}
|
||||
|
||||
private String calcSignature() throws Exception {
|
||||
incNonce();
|
||||
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
|
||||
sha256_HMAC.init(secret_key);
|
||||
|
||||
String data = nonce.toString() + userId + key;
|
||||
return Util.bytesToHex(sha256_HMAC.doFinal(data.getBytes("UTF-8")));
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestBalance(String cryptoCurrency, String fiatCurrency) throws Exception {
|
||||
|
||||
initApi();
|
||||
|
||||
String signature = calcSignature();
|
||||
|
||||
Log.e("CRYPTONIT", "user: " + userId);
|
||||
Log.e("CRYPTONIT", "key: " + key);
|
||||
Log.e("CRYPTONIT", "secret: " + secret);
|
||||
Log.e("CRYPTONIT", "nonce: " + nonce);
|
||||
Log.e("CRYPTONIT", "signature: " + signature);
|
||||
|
||||
api.getBalance(cryptoCurrency, fiatCurrency,
|
||||
RequestBody.create(MediaType.parse("text/plain"), key),
|
||||
RequestBody.create(MediaType.parse("text/plain"), signature),
|
||||
RequestBody.create(MediaType.parse("text/plain"), nonce.toString()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(balanceModel -> balanceListener.onBalanceData(balanceModel),
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestCryptoWithdrawal(String currency, String amount, String address) throws Exception {
|
||||
|
||||
initApi();
|
||||
|
||||
String signature = calcSignature();
|
||||
|
||||
Log.e("CRYPTONIT", "user: " + userId);
|
||||
Log.e("CRYPTONIT", "key: " + key);
|
||||
Log.e("CRYPTONIT", "secret: " + secret);
|
||||
Log.e("CRYPTONIT", "nonce: " + nonce);
|
||||
Log.e("CRYPTONIT", "signature: " + signature);
|
||||
|
||||
api.cryptoWithdrawal(
|
||||
RequestBody.create(MediaType.parse("text/plain"), currency),
|
||||
RequestBody.create(MediaType.parse("text/plain"), amount),
|
||||
RequestBody.create(MediaType.parse("text/plain"), address),
|
||||
RequestBody.create(MediaType.parse("text/plain"), key),
|
||||
RequestBody.create(MediaType.parse("text/plain"), signature),
|
||||
RequestBody.create(MediaType.parse("text/plain"), nonce.toString()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> {
|
||||
if (response.success != null && response.success)
|
||||
withdrawalListener.onWithdrawalComplete(response);
|
||||
else {
|
||||
LinkedTreeMap reason = (LinkedTreeMap) response.reason;
|
||||
errorListener.onError(new Exception(reason.entrySet().toArray()[0].toString()));
|
||||
}
|
||||
},
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
private void FireError(Throwable e) throws IOException {
|
||||
if (e.getClass() == HttpException.class && ((HttpException) e).code() == 500) {
|
||||
JsonObject jsonObject = new JsonParser().parse(((HttpException) e).response().errorBody().string()).getAsJsonObject();
|
||||
errorListener.onError(new Exception(e.getMessage() + ": " + jsonObject.get("reason").getAsString()));
|
||||
if (jsonObject.get("reason").getAsString().equals("Invalid nonce")) nonce += 1000;
|
||||
} else {
|
||||
errorListener.onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initApi() {
|
||||
|
||||
if (api != null) return;
|
||||
|
||||
// HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||||
// logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient httpClient = new OkHttpClient.Builder().
|
||||
// addInterceptor(logging).
|
||||
build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(SERVER_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(httpClient)
|
||||
.build();
|
||||
|
||||
api = retrofit.create(Api.class);
|
||||
}
|
||||
}
|
||||
358
app/src/main/java/com/tangem/data/network/Kraken.java
Normal file
358
app/src/main/java/com/tangem/data/network/Kraken.java
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
package com.tangem.data.network;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||
import com.tangem.util.Util;
|
||||
import com.tangem.wallet.R;
|
||||
|
||||
import org.spongycastle.util.encoders.Base64;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
//import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import okio.Buffer;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
/**
|
||||
* HTTP
|
||||
* Used in Kraken service
|
||||
*/
|
||||
public class Kraken {
|
||||
private static final String SERVER_URL = "https://api.kraken.com";
|
||||
|
||||
private static class Method {
|
||||
public static final String BALANCE = SERVER_URL + "/0/private/Balance";
|
||||
public static final String WITHDRAW_INFO = SERVER_URL + "/0/private/WithdrawInfo";
|
||||
public static final String WITHDRAW = SERVER_URL + "/0/private/Withdraw";
|
||||
}
|
||||
|
||||
public static class Model {
|
||||
|
||||
public static class Balance {
|
||||
public static class Response {
|
||||
public String[] error;
|
||||
public Result result;
|
||||
|
||||
public static class Result {
|
||||
public String XXBT;
|
||||
public String XETH;
|
||||
public String BCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WithdrawInfo {
|
||||
public static class Response {
|
||||
public String[] error;
|
||||
public Result result;
|
||||
|
||||
public static class Result {
|
||||
public String fee;
|
||||
public String amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Withdraw {
|
||||
public static class Response {
|
||||
public String[] error;
|
||||
public Result result;
|
||||
|
||||
public static class Result {
|
||||
public String refid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Api {
|
||||
@FormUrlEncoded
|
||||
@POST(Method.BALANCE)
|
||||
Observable<Model.Balance.Response> getBalance(@Field("nonce") String nonce);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(Method.WITHDRAW_INFO)
|
||||
Observable<Model.WithdrawInfo.Response> WithdrawInfo(@Field("nonce") String nonce, @Field("asset") String asset, @Field("key") String key, @Field("amount") String amount);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(Method.WITHDRAW)
|
||||
Observable<Model.Withdraw.Response> Withdraw(@Field("nonce") String nonce, @Field("asset") String asset, @Field("key") String key, @Field("amount") String amount);
|
||||
}
|
||||
|
||||
private Api api = null;
|
||||
private BalanceListener balanceListener;
|
||||
private WithdrawalListener withdrawalListener;
|
||||
private WithdrawalInfoListener withdrawalInfoListener;
|
||||
private ErrorListener errorListener;
|
||||
private Context context;
|
||||
|
||||
public Kraken(Context context) {
|
||||
this.context = context;
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
key = sp.getString(context.getResources().getString(R.string.key_kraken_key), "");
|
||||
secret = sp.getString(context.getResources().getString(R.string.key_kraken_secret), "");
|
||||
nonce = sp.getInt(context.getResources().getString(R.string.key_kraken_nonce), 0);
|
||||
}
|
||||
|
||||
public String key;
|
||||
public String secret;
|
||||
private Integer nonce;
|
||||
|
||||
public String getSecretDescription() {
|
||||
if (secret == null || secret.isEmpty()) return "";
|
||||
return secret.substring(0, 3) + "..." + secret.substring(secret.length() - 3, secret.length());
|
||||
}
|
||||
|
||||
public Boolean haveAccountInfo() {
|
||||
return (!key.isEmpty() && !secret.isEmpty());
|
||||
}
|
||||
|
||||
public void saveAccountInfo() {
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
sp.edit()
|
||||
.putString(context.getResources().getString(R.string.key_kraken_key), key)
|
||||
.putString(context.getResources().getString(R.string.key_kraken_secret), secret)
|
||||
.putInt(context.getResources().getString(R.string.key_kraken_nonce), nonce)
|
||||
.apply();
|
||||
|
||||
}
|
||||
|
||||
private void incNonce() {
|
||||
nonce++;
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
sp.edit().putInt(context.getResources().getString(R.string.key_kraken_nonce), nonce).apply();
|
||||
}
|
||||
|
||||
public interface BalanceListener {
|
||||
void onBalanceData(Model.Balance.Response response);
|
||||
}
|
||||
|
||||
public interface WithdrawalInfoListener {
|
||||
void onWithdrawalInfoComplete(Model.WithdrawInfo.Response response);
|
||||
}
|
||||
|
||||
public interface WithdrawalListener {
|
||||
void onWithdrawalComplete(Model.Withdraw.Response response);
|
||||
}
|
||||
|
||||
public interface ErrorListener {
|
||||
void onError(Throwable throwable);
|
||||
}
|
||||
|
||||
public void setBalanceListener(BalanceListener listener) {
|
||||
balanceListener = listener;
|
||||
}
|
||||
|
||||
public void setWithdrawalListener(WithdrawalListener listener) {
|
||||
withdrawalListener = listener;
|
||||
}
|
||||
|
||||
public void setWithdrawalInfoListener(WithdrawalInfoListener listener) {
|
||||
withdrawalInfoListener = listener;
|
||||
}
|
||||
|
||||
public void setErrorListener(ErrorListener listener) {
|
||||
errorListener = listener;
|
||||
}
|
||||
|
||||
|
||||
private static final String HMAC_SHA512 = "HmacSHA512";
|
||||
|
||||
|
||||
private String bodyToString(final RequestBody request){
|
||||
try {
|
||||
final RequestBody copy = request;
|
||||
final Buffer buffer = new Buffer();
|
||||
if(copy != null)
|
||||
copy.writeTo(buffer);
|
||||
else
|
||||
return "";
|
||||
return buffer.readUtf8();
|
||||
}
|
||||
catch (final IOException e) {
|
||||
return "did not work";
|
||||
}
|
||||
}
|
||||
|
||||
private String calcSignature(String url, RequestBody requestBody) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
|
||||
String postData=bodyToString(requestBody);
|
||||
|
||||
// create SHA-256 hash of the nonce and the POST data
|
||||
|
||||
String s=nonce+postData;
|
||||
byte[] sha256 = Util.calculateSHA256(s);
|
||||
|
||||
// set the API method and retrieve the path
|
||||
byte[] path = url.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// decode the API secret, it's the HMAC key
|
||||
byte[] hmacKey = Base64.decode(secret);
|
||||
|
||||
// create the HMAC message from the path and the previous hash
|
||||
ByteArrayOutputStream outputStream=new ByteArrayOutputStream();
|
||||
outputStream.write(path);
|
||||
outputStream.write(sha256);
|
||||
byte[] hmacMessage = outputStream.toByteArray();//concatArrays(path, sha256);
|
||||
|
||||
Mac mac = Mac.getInstance(HMAC_SHA512);
|
||||
mac.init(new SecretKeySpec(hmacKey, HMAC_SHA512));
|
||||
byte[] hmacSignature=mac.doFinal(hmacMessage);
|
||||
|
||||
byte[] b64Signature = Base64.encode(hmacSignature);
|
||||
|
||||
return new String(b64Signature, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestBalance() throws Exception {
|
||||
|
||||
initApi();
|
||||
|
||||
Log.e("kraken", "key: " + key);
|
||||
Log.e("kraken", "secret: " + secret);
|
||||
Log.e("kraken", "nonce: " + nonce);
|
||||
|
||||
api.getBalance(nonce.toString())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> balanceListener.onBalanceData(response),
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestWithdrawInfo(String currency, String amount, String withdrawKey) throws Exception {
|
||||
|
||||
initApi();
|
||||
|
||||
// Log.e("kraken", "key: " + key);
|
||||
// Log.e("kraken", "secret: " + secret);
|
||||
// Log.e("kraken", "nonce: " + nonce);
|
||||
|
||||
String asset=CurrencyToAsset(currency);
|
||||
|
||||
// withdrawKey = "test";
|
||||
api.WithdrawInfo(nonce.toString(), asset, withdrawKey, amount)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> withdrawalInfoListener.onWithdrawalInfoComplete(response),
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
private static String CurrencyToAsset(String currency) throws Exception {
|
||||
switch (currency)
|
||||
{
|
||||
case "BTC": return "XXBT";
|
||||
case "BCH": return "BCH";
|
||||
case "ETH": return "XETH";
|
||||
default:
|
||||
throw new Exception("Unsupported currency!");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
public void requestWithdraw(String currency, String amount, String withdrawKey) throws Exception {
|
||||
initApi();
|
||||
|
||||
// Log.e("kraken", "key: " + key);
|
||||
// Log.e("kraken", "secret: " + secret);
|
||||
// Log.e("kraken", "nonce: " + nonce);
|
||||
|
||||
String asset=CurrencyToAsset(currency);
|
||||
|
||||
// withdrawKey = "test";
|
||||
api.Withdraw(nonce.toString(), asset, withdrawKey, amount)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(response -> withdrawalListener.onWithdrawalComplete(response),
|
||||
// handle error
|
||||
this::FireError
|
||||
);
|
||||
}
|
||||
|
||||
private void FireError(Throwable e) throws IOException {
|
||||
// if (e.getClass() == HttpException.class && ((HttpException) e).code() == 500) {
|
||||
// JsonObject jsonObject = new JsonParser().parse(((HttpException) e).response().errorBody().string()).getAsJsonObject();
|
||||
// errorListener.onError(new Exception(e.getMessage() + ": " + jsonObject.get("reason").getAsString()));
|
||||
// if (jsonObject.get("reason").getAsString().equals("Invalid nonce")) nonce += 1000;
|
||||
// } else {
|
||||
errorListener.onError(e);
|
||||
// }
|
||||
}
|
||||
|
||||
private void initApi() {
|
||||
|
||||
if (api != null) return;
|
||||
|
||||
// HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||||
// logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient httpClient = new OkHttpClient.Builder().
|
||||
addInterceptor(new AuthorizationInterceptor()).
|
||||
// addInterceptor(logging).
|
||||
build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(SERVER_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(httpClient)
|
||||
.build();
|
||||
|
||||
api = retrofit.create(Api.class);
|
||||
}
|
||||
|
||||
public class AuthorizationInterceptor implements Interceptor {
|
||||
|
||||
AuthorizationInterceptor() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request mainRequest=chain.request();
|
||||
|
||||
try {
|
||||
mainRequest = mainRequest.newBuilder().
|
||||
addHeader("API-Key", key).
|
||||
addHeader("API-Sign", calcSignature(mainRequest.url().toString().substring(SERVER_URL.length()), mainRequest.body())).
|
||||
build();
|
||||
incNonce();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new IOException("Can't calculate signature: "+e.getMessage());
|
||||
}
|
||||
|
||||
return chain.proceed(mainRequest);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue