package com.greenaddress.greenbits;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.util.SparseArray;
import com.blockstream.libwally.Wally;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.primitives.UnsignedLongs;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.greenaddress.greenapi.ConfidentialAddress;
import com.greenaddress.greenapi.CryptoHelper;
import com.greenaddress.greenapi.ElementsRegTestParams;
import com.greenaddress.greenapi.HDClientKey;
import com.greenaddress.greenapi.HDKey;
import com.greenaddress.greenapi.INotificationHandler;
import com.greenaddress.greenapi.ISigningWallet;
import com.greenaddress.greenapi.JSONMap;
import com.greenaddress.greenapi.LoginData;
import com.greenaddress.greenapi.Network;
import com.greenaddress.greenapi.Output;
import com.greenaddress.greenapi.PinData;
import com.greenaddress.greenapi.PreparedTransaction;
import com.greenaddress.greenapi.SWWallet;
import com.greenaddress.greenapi.WalletClient;
import com.greenaddress.greenbits.spv.SPV;
import com.greenaddress.greenbits.ui.BuildConfig;
import com.greenaddress.greenbits.ui.R;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.bitcoinj.utils.MonetaryFormat;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class GaService extends Service implements INotificationHandler {
private static final String TAG = GaService.class.getSimpleName();
public static final boolean IS_ELEMENTS = Network.NETWORK == ElementsRegTestParams.get();
private enum ConnState {
OFFLINE, DISCONNECTED, CONNECTING, CONNECTED, LOGGINGIN, LOGGEDIN
}
class GaBinder extends Binder {
GaService getService() { return GaService.this; }
}
private final IBinder mBinder = new GaBinder();
@Override
public IBinder onBind(final Intent intent) { return mBinder; }
public void onBound(final GreenAddressApplication app) {
// Update our state when network connectivity changes.
mNetConnectivityReceiver = new BroadcastReceiver() {
public void onReceive(final Context context, final Intent intent) {
onNetConnectivityChanged();
}
};
app.registerReceiver(mNetConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
// Fire a fake connectivity change to kick start the state machine
mNetConnectivityReceiver.onReceive(null, null);
}
private final ListeningExecutorService mExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(3));
public ListenableFuture<Void> onConnected;
private final SparseArray<GaObservable> mBalanceObservables = new SparseArray<>();
private final GaObservable mNewTxObservable = new GaObservable();
private final GaObservable mVerifiedTxObservable = new GaObservable();
private String mSignUpMnemonics;
private Bitmap mSignUpQRCode;
private int mCurrentBlock; // FIXME: Pass current block height back in login data.
private boolean mAutoReconnect = true;
// cache
private ListenableFuture<List<List<String>>> mCurrencyExchangePairs;
private final SparseArray<Coin> mCoinBalances = new SparseArray<>();
private Float mFiatRate;
private String mFiatCurrency;
private String mFiatExchange;
private ArrayList<Map<String, Object>> mSubAccounts;
private String mReceivingId;
private Coin mDustThreshold = Coin.valueOf(546); // Per 0.13.0, updated on login
private Coin mMinFeeRate = Coin.valueOf(1000); // Per 0.12.0, updated on login
private Map<?, ?> mTwoFactorConfig;
private final GaObservable mTwoFactorConfigObservable = new GaObservable();
private String mDeviceId;
private boolean mUserCancelledPINEntry;
public byte[] mAssetId;
private String mAssetSymbol;
private MonetaryFormat mAssetFormat;
private final SPV mSPV = new SPV(this);
private WalletClient mClient;
public ListeningExecutorService getExecutor() {
return mExecutor;
}
public ISigningWallet getSigningWallet() {
return mClient.getSigningWallet();
}
public String getBitcoinUnit() {
final Object unit = getUserConfig("unit");
return unit == null ? "bits" : (String) unit;
}
public int getAutoLogoutMinutes() {
try {
return (int)getUserConfig("altimeout");
} catch (final Exception e) {
return 5; // Not logged in/not set, default to 5 min
}
}
public File getSPVChainFile() {
final String dirName = "blockstore_" + mReceivingId;
return new File(getDir(dirName, Context.MODE_PRIVATE), "blockchain.spvchain");
}
private void getAvailableTwoFactorMethods() {
Futures.addCallback(mClient.getTwoFactorConfig(), new FutureCallback<Map<?, ?>>() {
@Override
public void onSuccess(final Map<?, ?> result) {
mTwoFactorConfig = result;
mTwoFactorConfigObservable.doNotify();
}
@Override
public void onFailure(final Throwable t) {
t.printStackTrace();
}
}, mExecutor);
}
public boolean getUserCancelledPINEntry() {
return mUserCancelledPINEntry;
}
public void setUserCancelledPINEntry(final boolean value) {
mUserCancelledPINEntry = value;
}
private void reloadSettings() {
mClient.setProxy(getProxyHost(), getProxyPort());
mClient.setTorEnabled(getTorEnabled());
}
private void reconnect() {
reloadSettings();
Log.i(TAG, "Submitting reconnect after " + mReconnectDelay);
onConnected = mClient.connect();
mState.transitionTo(ConnState.CONNECTING);
Futures.addCallback(onConnected, new FutureCallback<Void>() {
@Override
public void onSuccess(final Void result) {
mState.transitionTo(ConnState.CONNECTED);
Log.i(TAG, "Success CONNECTED callback");
if (mState.isForcedOff())
return;
try {
if (isWatchOnly())
loginImpl(mClient.watchOnlylogin(mClient.getWatchOnlyUsername(), mClient.getWatchOnlyPassword()));
else if (mClient.getSigningWallet() != null)
loginImpl(mClient.login(mClient.getSigningWallet(), mDeviceId, null));
} catch (final Exception e) {
e.printStackTrace();
this.onFailure(e);
}
}
@Override
public void onFailure(final Throwable t) {
t.printStackTrace();
Log.i(TAG, "Failure throwable callback " + t.toString());
mState.transitionTo(ConnState.DISCONNECTED);
scheduleReconnect();
}
}, mExecutor);
}
public static boolean isValidAddress(final String address) {
try {
if (IS_ELEMENTS)
ConfidentialAddress.fromBase58(Network.NETWORK, address);
else
Address.fromBase58(Network.NETWORK, address);
return true;
} catch (final AddressFormatException e) {
return false;
}
}
public boolean isWatchOnly() {
return mClient.isWatchOnly();
}
public boolean isSegwitUnconfirmed() {
return mClient.isSegwitUnconfirmed();
}
public boolean isSegwitEnabled() {
return mClient.isSegwitEnabled();
}
// Sugar for fetching/editing preferences
public SharedPreferences cfg() { return PreferenceManager.getDefaultSharedPreferences(this); }
public SharedPreferences cfg(final String name) { return getSharedPreferences(name, MODE_PRIVATE); }
public SharedPreferences.Editor cfgEdit(final String name) { return cfg(name).edit(); }
public SharedPreferences cfgIn(final String name) { return cfg(name + mReceivingId); }
public SharedPreferences.Editor cfgInEdit(final String name) { return cfgIn(name).edit(); }
// User config is stored on the server (unlike preferences which are local)
public Object getUserConfig(final String key) {
return mClient.getUserConfig(key);
}
public String getProxyHost() { return cfg().getString("proxy_host", ""); }
public String getProxyPort() { return cfg().getString("proxy_port", ""); }
private boolean getTorEnabled() { return cfg().getBoolean("tor_enabled", false); }
public boolean isSegwitUnlocked() { return !cfgIn("CONFIG").getBoolean("sw_locked", false); }
private void setSegwitLocked() { cfgInEdit("CONFIG").putBoolean("sw_locked", true).apply(); }
public boolean isProxyEnabled() { return !TextUtils.isEmpty(getProxyHost()) && !TextUtils.isEmpty(getProxyPort()); }
public int getCurrentSubAccount() { return cfgIn("CONFIG").getInt("current_subaccount", 0); }
public void setCurrentSubAccount(final int subAccount) { cfgInEdit("CONFIG").putInt("current_subaccount", subAccount).apply(); }
public boolean showBalanceInTitle() { return cfg().getBoolean("show_balance_in_title", false); }
// SPV
public String getSPVTrustedPeers() { return mSPV.getTrustedPeers(); }
public void setSPVTrustedPeersAsync(final String peers) { mSPV.setTrustedPeersAsync(peers); }
public boolean isSPVEnabled() { return mSPV.isEnabled(); }
public void setSPVEnabledAsync(final boolean enabled) { mSPV.setEnabledAsync(enabled); }
public boolean isSPVSyncOnMobileEnabled() { return mSPV.isSyncOnMobileEnabled(); }
public void setSPVSyncOnMobileEnabledAsync(final boolean enabled) { mSPV.setSyncOnMobileEnabledAsync(enabled); }
public void resetSPVAsync() { mSPV.resetAsync(); }
public PeerGroup getSPVPeerGroup() { return mSPV.getPeerGroup(); }
public int getSPVHeight() { return mSPV.getSPVHeight(); }
public int getSPVBlocksRemaining() { return mSPV.getSPVBlocksRemaining(); }
public Coin getSPVVerifiedBalance(final int subAccount) {
final Coin balance = mSPV.getVerifiedBalance(subAccount);
return balance == null ? Coin.ZERO : balance;
}
public boolean isSPVVerified(final Sha256Hash txHash) { return mSPV.isVerified(txHash); }
public void enableSPVPingMonitoring() { mSPV.enablePingMonitoring(); }
public void disableSPVPingMonitoring() { mSPV.disablePingMonitoring(); }
public static boolean isBadAddress(final String s) {
if (s.isEmpty())
return false;
try {
new URI("btc://" + s);
return false;
} catch (final URISyntaxException e) {
}
return true;
}
@Override
public void onCreate() {
super.onCreate();
// Uncomment to test slow service creation
// android.os.SystemClock.sleep(10000);
mTimerExecutor = new ScheduledThreadPoolExecutor(1);
mTimerExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
mDeviceId = cfg("service").getString("device_id", null);
if (mDeviceId == null) {
// Generate a unique device id
mDeviceId = UUID.randomUUID().toString();
cfgEdit("service").putString("device_id", mDeviceId).apply();
}
mClient = new WalletClient(this, mExecutor);
}
@Override
public void onNewBlock(final int blockHeight) {
Log.i(TAG, "onNewBlock");
setCurrentBlock(blockHeight);
mSPV.onNewBlock(blockHeight);
mNewTxObservable.doNotify();
}
@Override
public void onNewTransaction(final int[] affectedSubAccounts) {
Log.i(TAG, "onNewTransaction");
mSPV.updateUnspentOutputs();
mNewTxObservable.doNotify();
for (final int subAccount : affectedSubAccounts)
updateBalance(subAccount);
}
@Override
public void onConnectionClosed(final int code) {
HDKey.resetCache(null);
HDClientKey.resetCache(null, null);
// Server error codes FIXME: These should be in a class somewhere
// 4000 (concurrentLoginOnDifferentDeviceId) && 4001 (concurrentLoginOnSameDeviceId!)
// 1000 NORMAL_CLOSE
// 1006 SERVER_RESTART
mState.setForcedLogout(code == 4000);
mState.transitionTo(ConnState.DISCONNECTED);
if (getNetworkInfo() == null) {
mState.transitionTo(ConnState.OFFLINE);
return;
}
Log.i(TAG, "onConnectionClosed code=" + String.valueOf(code));
// FIXME: some callback to UI so you see what's happening.
mReconnectDelay = 0;
if (mAutoReconnect)
reconnect();
}
public byte[] createOutScript(final int subAccount, final Integer pointer) {
final List<ECKey> pubkeys = new ArrayList<>();
pubkeys.add(HDKey.getGAPublicKeys(subAccount, pointer)[1]);
pubkeys.add(HDClientKey.getMyPublicKey(subAccount, pointer));
final Map<String, Object> m = findSubaccountByType(subAccount, "2of3");
if (m != null)
pubkeys.add(HDKey.getRecoveryKeys((String) m.get("2of3_backup_chaincode"),
(String) m.get("2of3_backup_pubkey"), pointer)[1]);
return Script.createMultiSigOutputScript(2, pubkeys);
}
private ListenableFuture<Boolean> verifyP2SHSpendableBy(final Script scriptHash, final int subAccount, final Integer pointer) {
if (!scriptHash.isPayToScriptHash())
return Futures.immediateFuture(false);
final byte[] gotP2SH = scriptHash.getPubKeyHash();
return mExecutor.submit(new Callable<Boolean>() {
@Override
public Boolean call() {
final byte[] multisig = createOutScript(subAccount, pointer);
if (isSegwitEnabled() &&
Arrays.equals(gotP2SH, Utils.sha256hash160(getSegWitScript(multisig))))
return true;
return Arrays.equals(gotP2SH, Utils.sha256hash160(multisig));
}
});
}
public ListenableFuture<Boolean> verifySpendableBy(final TransactionOutput txOutput, final int subAccount, final Integer pointer) {
return verifyP2SHSpendableBy(txOutput.getScriptPubKey(), subAccount, pointer);
}
public String getWatchOnlyUsername() throws Exception {
return mClient.getWatchOnlyUsername();
}
public void registerWatchOnly(final String username, final String password) throws Exception {
mClient.registerWatchOnly(username, password);
}
private ListenableFuture<LoginData> loginImpl(final ListenableFuture<LoginData> loginFn) {
mState.transitionTo(ConnState.LOGGINGIN);
// Chain the login and post-login processing together, so any
// callbacks added by the caller are executed only once our post
// login processing is completed.
final ListenableFuture fn = Futures.transform(loginFn, new Function<LoginData, LoginData>() {
@Override
public LoginData apply(final LoginData loginData) {
onPostLogin(loginData);
return loginData;
}
});
// Add a callback to set our state back to connected if an error
// occurs. Ideally we could add this in transform(), but it doesnt
// seem possible. So there is a delay before our state is updated.
Futures.addCallback(fn, new FutureCallback<LoginData>() {
@Override
public void onSuccess(final LoginData result) { }
@Override
public void onFailure(final Throwable t) {
t.printStackTrace();
mState.transitionTo(ConnState.CONNECTED);
}
}, mExecutor);
return fn;
}
private void onPostLogin(final LoginData loginData) {
// Uncomment to test slow login post processing
// android.os.SystemClock.sleep(10000);
Log.d(TAG, "Success LOGIN callback");
// FIXME: Why are we copying these? If we need them when not logged in,
// we should just copy the whole loginData instance
mFiatCurrency = loginData.get("currency");
mFiatExchange = loginData.get("exchange");
mSubAccounts = loginData.mSubAccounts;
mReceivingId = loginData.get("receiving_id");
if (loginData.mRawData.containsKey("min_fee"))
mMinFeeRate = Coin.valueOf((long)((int) loginData.get("min_fee")));
if (loginData.mRawData.containsKey("dust"))
mDustThreshold = Coin.valueOf((long) ((int) loginData.get("dust")));
HDKey.resetCache(loginData.mGaitPath);
mBalanceObservables.put(0, new GaObservable());
if (IS_ELEMENTS) {
// ignore login data from elements since it doesn't include confidential values
updateBalance(0);
for (final Map<String, Object> data : mSubAccounts)
updateBalance((Integer) data.get("pointer"));
// fetch the asset id and symbol for elements:
int maxId = 0;
final Map<String, Integer> assetIds = (Map<String, Integer>) loginData.mRawData.get("asset_ids");
final Map<String, String> assetSymbols = (Map<String, String>) loginData.mRawData.get("asset_symbols");
for (final String assetIdHex : assetIds.keySet()) {
// find largest asset id that has a symbol set:
if (assetIds.get(assetIdHex) > maxId && assetSymbols.get(assetIdHex) != null) {
maxId = assetIds.get(assetIdHex);
mAssetId = Wally.hex_to_bytes(assetIdHex);
}
}
mAssetSymbol = assetSymbols.get(
Wally.hex_from_bytes(mAssetId)
);
final int decimalPlaces = ((Map<String, Integer>) loginData.mRawData.get("asset_decimal_places")).get(
Wally.hex_from_bytes(mAssetId)
);
mAssetFormat = new MonetaryFormat().shift(8 - decimalPlaces).minDecimals(decimalPlaces).noCode();
} else {
updateBalance(0, loginData.mRawData);
for (final Map<String, Object> data : mSubAccounts) {
final int pointer = ((Integer) data.get("pointer"));
mBalanceObservables.put(pointer, new GaObservable());
updateBalance(pointer, data);
}
}
if (!isWatchOnly()) {
getAvailableTwoFactorMethods();
mSPV.startAsync();
}
mState.transitionTo(ConnState.LOGGEDIN);
}
public ListenableFuture<LoginData> login(final ISigningWallet signingWallet) {
return loginImpl(mClient.login(signingWallet, mDeviceId, null));
}
public ListenableFuture<LoginData> watchOnlyLogin(final String username, final String password) {
return loginImpl(mClient.watchOnlylogin(username, password));
}
public void disableWatchOnly() throws Exception {
mClient.disableWatchOnly();
}
public ListenableFuture<LoginData> login(final String mnemonics) {
return login(new SWWallet(mnemonics), mnemonics);
}
private ListenableFuture<LoginData> login(final ISigningWallet signingWallet, final String mnemonics) {
return loginImpl(mClient.login(signingWallet, mDeviceId, mnemonics));
}
private ListenableFuture<LoginData> signup(final ISigningWallet signingWallet,
final String mnemonics,
final byte[] pubkey, final byte[] chaincode,
final byte[] pathPubkey, final byte[] pathChaincode) {
mState.transitionTo(ConnState.LOGGINGIN);
return mExecutor.submit(new Callable<LoginData>() {
@Override
public LoginData call() throws Exception {
try {
mClient.registerUser(signingWallet, mnemonics,
pubkey, chaincode,
pathPubkey, pathChaincode,
mDeviceId);
onPostLogin(mClient.getLoginData());
return mClient.getLoginData();
} catch (final Exception e) {
e.printStackTrace();
mState.transitionTo(ConnState.CONNECTED);
throw e;
}
}
});
}
public ListenableFuture<LoginData> signup(final String mnemonics) {
final SWWallet sw = new SWWallet(mnemonics);
return signup(sw, mnemonics, sw.getMasterKey().getPubKey(),
sw.getMasterKey().getChainCode(), null, null);
}
public ListenableFuture<LoginData> signup(final ISigningWallet signingWallet,
final byte[] pubkey, final byte[] chaincode,
final byte[] pathPubkey, final byte[] pathChaincode) {
return signup(signingWallet, null, pubkey, chaincode, pathPubkey, pathChaincode);
}
public String getMnemonics() {
return mClient.getMnemonics();
}
public LoginData getLoginData() {
return mClient.getLoginData();
}
public Map<String, Object> getFeeEstimates() {
return mClient.getFeeEstimates();
}
public Coin getMinFeeRate() {
return mMinFeeRate;
}
public Coin getDustThreshold() {
return mDustThreshold;
}
public void disconnect(final boolean autoReconnect) {
mAutoReconnect = autoReconnect;
mSPV.stopSyncAsync();
final int size = mBalanceObservables.size();
for(int i = 0; i < size; ++i) {
final int key = mBalanceObservables.keyAt(i);
mBalanceObservables.get(key).deleteObservers();
}
mClient.disconnect();
mState.transitionTo(ConnState.DISCONNECTED);
}
public void updateBalance(final int subAccount) {
Futures.addCallback(getSubaccountBalance(subAccount), new FutureCallback<Map<String, Object>>() {
@Override
public void onSuccess(final Map<String, Object> data) {
updateBalance(subAccount, data);
}
@Override
public void onFailure(final Throwable t) { }
}, mExecutor);
}
private void updateBalance(final int subAccount, final Map<String, Object> rawData) {
final JSONMap data = new JSONMap(rawData);
final String fiatCurrency = data.getString("fiat_currency");
if (!TextUtils.isEmpty(fiatCurrency))
mFiatCurrency = fiatCurrency;
mCoinBalances.put(subAccount, data.getCoin("satoshi"));
try {
mFiatRate = data.getFloat("fiat_exchange");
} catch (final java.lang.NumberFormatException e) {
Log.d(TAG, "No exchange rate returned by server");
}
fireBalanceChanged(subAccount);
}
public ListenableFuture<Map<String, Object>> getSubaccountBalance(final int subAccount) {
if (IS_ELEMENTS)
return getBalanceFromUtxo(subAccount);
return mClient.getSubaccountBalance(subAccount);
}
private ListenableFuture<Map<String,Object>> getBalanceFromUtxo(final int subAccount) {
final boolean filterAsset = true;
return Futures.transform(getAllUnspentOutputs(0, subAccount, filterAsset),
new Function<List<JSONMap>, Map<String, Object>>() {
@Override
public Map<String, Object> apply(final List<JSONMap> utxos) {
final Map <String, Object> res = new HashMap<>();
res.put("fiat_currency", "?");
res.put("fiat_exchange", "1");
res.put("fiat_value", "0");
BigInteger finalValue = BigInteger.ZERO;
for (final JSONMap utxo : utxos)
finalValue = finalValue.add(utxo.getBigInteger("value"));
res.put("satoshi", String.valueOf(finalValue));
return res;
}
});
}
public void fireBalanceChanged(final int subAccount) {
if (getCoinBalance(subAccount) == null) {
// Called from addUtxoToValues before balance is fetched
return;
}
mBalanceObservables.get(subAccount).doNotify();
}
public void setPricingSource(final String currency, final String exchange) {
Futures.transform(mClient.setPricingSource(currency, exchange), new Function<Boolean, Boolean>() {
@Override
public Boolean apply(final Boolean input) {
mFiatCurrency = currency;
mFiatExchange = exchange;
return input;
}
});
}
public ListenableFuture<Map<String, Object>> getMyTransactions(final int subAccount) {
return mExecutor.submit(new Callable<Map<String, Object>>() {
@Override
public Map<String, Object> call() throws Exception {
final Map<String, Object> result = mClient.getMyTransactions(null, subAccount);
setCurrentBlock((Integer) result.get("cur_block"));
final List<JSONMap> txs = JSONMap.fromList((List) result.get("list"));
for (final JSONMap tx : txs)
tx.mData.put("eps", unblindValues(JSONMap.fromList((List) tx.get("eps")), false, false));
result.put("list", txs);
return result;
}
});
}
public ListenableFuture<Void> setPin(final String mnemonic, final String pin) {
return mExecutor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
final PinData pinData = mClient.setPin(mnemonic, pin, "default");
// As this is a new PIN, save it to config
final String encrypted = Base64.encodeToString(pinData.mSalt, Base64.NO_WRAP) + ';' +
Base64.encodeToString(pinData.mEncryptedData, Base64.NO_WRAP);
cfgEdit("pin").putString("ident", pinData.mPinIdentifier)
.putInt("counter", 0)
.putString("encrypted", encrypted)
.apply();
return null;
}
});
}
public ListenableFuture<LoginData> pinLogin(final String pin) throws Exception {
final String pinIdentifier = cfg("pin").getString("ident", null);
final byte[] password = mClient.getPinPassword(pinIdentifier, pin);
final String[] split = cfg("pin").getString("encrypted", null).split(";");
final byte[] salt = split[0].getBytes();
final byte[] encryptedData = Base64.decode(split[1], Base64.NO_WRAP);
final PinData pinData = PinData.fromEncrypted(pinIdentifier, salt, encryptedData, password);
final DeterministicKey master = HDKey.createMasterKeyFromSeed(pinData.mSeed);
return login(new SWWallet(master), pinData.mMnemonic);
}
private void preparePrivData(final JSONMap privateData) {
int subAccount = privateData.get("subaccount", 0);
// Skip fetching raw previous outputs if they are not required
final Coin verifiedBalance = getSPVVerifiedBalance(subAccount);
final boolean fetchPrev = !isSPVEnabled() ||
!verifiedBalance.equals(getCoinBalance(subAccount)) ||
mClient.getSigningWallet().requiresPrevoutRawTxs();
final boolean isRegTest = Network.NETWORK == RegTestParams.get();
final String fetchMode = isRegTest ? "" : "http"; // Fetch inline for regtest
privateData.mData.put("prevouts_mode", fetchPrev ? fetchMode : "skip");
final Object rbf_optin = getUserConfig("replace_by_fee");
if (rbf_optin != null)
privateData.mData.put("rbf_optin", rbf_optin);
}
public ListenableFuture<List<byte[]>> signTransaction(final PreparedTransaction ptx) {
return mClient.signTransaction(mClient.getSigningWallet(), ptx);
}
public List<byte[]> signTransaction(final Transaction tx, final PreparedTransaction ptx, final List<Output> prevOuts) {
return mClient.getSigningWallet().signTransaction(tx, ptx, prevOuts);
}
public ListenableFuture<Coin>
validateTx(final PreparedTransaction ptx, final String recipientStr, final Coin amount) {
return mSPV.validateTx(ptx, recipientStr, amount);
}
public ListenableFuture<String> signAndSendTransaction(final PreparedTransaction ptx, final Object twoFacData) {
return Futures.transform(signTransaction(ptx), new AsyncFunction<List<byte[]>, String>() {
@Override
public ListenableFuture<String> apply(final List<byte[]> txSigs) throws Exception {
return mClient.sendTransaction(txSigs, twoFacData);
}
}, mExecutor);
}
public ListenableFuture<Map<String, Object>> sendRawTransaction(final Transaction tx, final Map<String, Object> twoFacData, final JSONMap privateData, final boolean returnErrorUri) {
return mClient.sendRawTransaction(tx, twoFacData, privateData, returnErrorUri);
}
private List<JSONMap> unblindValues(final List<JSONMap> values, final boolean filterAsset,
final boolean isUtxo) {
if (!IS_ELEMENTS)
return values;
final List<JSONMap> result = new ArrayList<>(values.size());
final List<byte[]> unblinded = new ArrayList<>(3);
for (final JSONMap v : values) {
if ((isUtxo && v.get("value") == null) ||
(!isUtxo && v.get("commitment") != null)) {
// Blinded value: Unblind it
final Long value;
unblinded.clear();
value = Wally.asset_unblind(v.getBytes("nonce_commitment"),
getBlindingPrivKey(v),
v.getBytes("range_proof"),
v.getBytes("commitment"),
v.getBytes("asset_tag"),
unblinded);
final byte[] assetId = unblinded.get(0);
if (!Arrays.equals(assetId, mAssetId)) {
if (filterAsset)
continue; // Ignore
if (!isUtxo)
v.mData.put("is_relevant", false); // Mark irrelevant
}
v.mData.put("confidential", true);
v.mData.put("value", UnsignedLongs.toString(value));
if (isUtxo) {
v.putBytes("assetId", assetId);
v.putBytes("abf", unblinded.get(1));
v.putBytes("vbf", unblinded.get(2));
}
}
result.add(v);
}
return result;
}
public ListenableFuture<List<JSONMap>> getAllUnspentOutputs(final int confs, final Integer subAccount,
final boolean filterAsset) {
return Futures.transform(mClient.getAllUnspentOutputs(confs, subAccount),
new Function<List<JSONMap>, List<JSONMap>>() {
@Override
public List<JSONMap> apply(final List<JSONMap> utxos) {
return unblindValues(utxos, filterAsset, true);
}
});
}
public ListenableFuture<Transaction> getRawUnspentOutput(final Sha256Hash txHash) {
return mClient.getRawUnspentOutput(txHash);
}
public ListenableFuture<Transaction> getRawOutput(final Sha256Hash txHash) {
return mClient.getRawOutput(txHash);
}
public String getRawOutputHex(final Sha256Hash txHash) throws Exception {
return mClient.getRawOutputHex(txHash);
}
public ListenableFuture<Boolean> changeMemo(final Sha256Hash txHash, final String memo) {
return mClient.changeMemo(txHash, memo);
}
public ListenableFuture<String> sendTransaction(final List<byte[]> txSigs) {
return mClient.sendTransaction(txSigs, null);
}
private static byte[] getSegWitScript(final byte[] input) {
final ByteArrayOutputStream bits = new ByteArrayOutputStream();
bits.write(0);
try {
Script.writeBytes(bits, Wally.sha256(input));
} catch (final IOException e) {
throw new RuntimeException(e); // cannot happen
}
return bits.toByteArray();
}
public JSONMap getNewAddress(final int subAccount) {
final boolean userSegwit = isSegwitEnabled();
if (userSegwit && isSegwitUnlocked())
setSegwitLocked(); // Locally store that we have generated a SW address
return mClient.getNewAddress(subAccount, userSegwit ? "p2wsh" : "p2sh");
}
private void storeCachedAddress(final int subAccount, final byte[] salt, final byte[] encryptedAddress) {
final String configKey = "next_addr_" + subAccount;
cfgInEdit(configKey).putString("salt", Wally.hex_from_bytes(salt))
.putString("encrypted", Wally.hex_from_bytes(encryptedAddress))
.apply();
}
private void cacheAddress(final int subAccount, final JSONMap address) {
final byte[] password = getSigningWallet().getLocalEncryptionPassword();
final byte[] salt = CryptoHelper.randomBytes(16);
storeCachedAddress(subAccount, salt, CryptoHelper.encryptJSON(address, password, salt));
}
private void uncacheAddress(final int subAccount) {
storeCachedAddress(subAccount, new byte[] { 0 }, new byte[] { 0 });
}
private JSONMap getCachedAddress(final int subAccount) {
if (isWatchOnly())
return null;
final String configKey = "next_addr_" + subAccount;
final String saltHex = cfgIn(configKey).getString("salt", null);
if (saltHex == null || saltHex.length() != 32)
return null;
final String encryptedAddressHex = cfgIn(configKey).getString("encrypted", null);
JSONMap json;
try {
json = CryptoHelper.decryptJSON(Wally.hex_to_bytes(encryptedAddressHex),
getSigningWallet().getLocalEncryptionPassword(),
Wally.hex_to_bytes(saltHex));
final String expectedType = isSegwitEnabled() ? "p2wsh" : "p2sh";
if (!json.getString("addr_type").equals(expectedType))
json = null; // User has enabled SW, cached address is non-SW
} catch (final RuntimeException e) {
e.printStackTrace();
json = null;
}
uncacheAddress(subAccount);
return json;
}
private ListenableFuture<JSONMap> getNewAddressAsync(final int subAccount, final boolean cacheResult) {
return mExecutor.submit(new Callable<JSONMap>() {
@Override
public JSONMap call() throws Exception {
final JSONMap address = getNewAddress(subAccount);
if (cacheResult)
cacheAddress(subAccount, address);
return address;
}
});
}
public ListenableFuture<QrBitmap> getNewAddressBitmap(final int subAccount,
final Callable<Void> waitFn,
final Long amount) {
// Fetch any cached address
final JSONMap cachedAddress = getCachedAddress(subAccount);
// Use either the cached address or a new address
final ListenableFuture<JSONMap> addrFn;
if (cachedAddress != null)
addrFn = Futures.immediateFuture(cachedAddress);
else {
try {
waitFn.call();
} catch (final Exception e) {
}
addrFn = getNewAddressAsync(subAccount, false);
}
// Fetch and cache another address in the background
if (!isWatchOnly())
getNewAddressAsync(subAccount, true);
// Convert the address into a bitmap and return it
final AsyncFunction<JSONMap, QrBitmap> verifyAddress = new AsyncFunction<JSONMap, QrBitmap>() {
@Override
public ListenableFuture<QrBitmap> apply(final JSONMap input) throws Exception {
if (input == null)
throw new IllegalArgumentException("Failed to generate a new address");
final Integer pointer = input.getInt("pointer");
final byte[] script = input.getBytes("script");
final byte[] scriptHash;
if (isSegwitEnabled())
scriptHash = Utils.sha256hash160(getSegWitScript(script));
else
scriptHash = Utils.sha256hash160(script);
final ListenableFuture<Boolean> verify;
if (isWatchOnly())
verify = Futures.immediateFuture(true);
else {
final Script sc;
sc = ScriptBuilder.createP2SHOutputScript(scriptHash);
verify = verifyP2SHSpendableBy(sc, subAccount, pointer);
}
return Futures.transform(verify,
new Function<Boolean, QrBitmap>() {
@Override
public QrBitmap apply(final Boolean isValid) {
if (!isValid)
throw new IllegalArgumentException("Address validation failed");
final String address;
if (IS_ELEMENTS) {
final byte[] pubKey = getBlindingPubKey(subAccount, pointer);
address = ConfidentialAddress.fromP2SHHash(Network.NETWORK, scriptHash, pubKey).toString();
} else
address = Address.fromP2SHHash(Network.NETWORK, scriptHash).toString();
final String uri;
if (amount != null)
uri = "bitcoin:" + address + "?amount=" + Coin.valueOf(amount).toPlainString();
else
uri = address;
return new QrBitmap(uri, 0 /* transparent background */);
}
});
}
};
return Futures.transform(addrFn, verifyAddress, mExecutor);
}
public byte[] getBlindingPubKey(final int subAccount, final int pointer) {
return Wally.ec_public_key_from_private_key(getBlindingPrivKey(subAccount, pointer));
}
private byte[] getBlindingPrivKey(final int subAccount, final int pointer) {
final byte[] privKey = new byte[32];
// TODO derive real blinding key
for (int i = 0; i < 32; ++i)
privKey[i] = 1;
return privKey;
}
// Fetch a blinding key from a utxo (or endpoint)
private byte[] getBlindingPrivKey(final JSONMap utxo) {
return getBlindingPrivKey(utxo.getInt("subaccount", 0),
utxo.getInt(utxo.getKey("pubkey_pointer", "pointer")));
}
public ListenableFuture<List<List<String>>> getCurrencyExchangePairs() {
if (mCurrencyExchangePairs == null) {
mCurrencyExchangePairs = Futures.transform(mClient.getAvailableCurrencies(), new Function<Map<?, ?>, List<List<String>>>() {
@Override
public List<List<String>> apply(final Map<?, ?> result) {
final Map<String, ArrayList<String>> per_exchange = (Map) result.get("per_exchange");
final List<List<String>> ret = new LinkedList<>();
for (final String exchange : per_exchange.keySet()) {
for (final String currency : per_exchange.get(exchange))
ret.add(Lists.newArrayList(currency, exchange));
}
Collections.sort(ret, new Comparator<List<String>>() {
@Override
public int compare(final List<String> lhs, final List<String> rhs) {
return lhs.get(0).compareTo(rhs.get(0));
}
});
return ret;
}
}, mExecutor);
}
return mCurrencyExchangePairs;
}
public void resetSignUp() {
mSignUpMnemonics = null;
if (mSignUpQRCode != null)
mSignUpQRCode.recycle();
mSignUpQRCode = null;
}
public String getSignUpMnemonic() {
if (mSignUpMnemonics == null)
mSignUpMnemonics = CryptoHelper.mnemonic_from_bytes(CryptoHelper.randomBytes(32));
return mSignUpMnemonics;
}
public Bitmap getSignUpQRCode() {
if (mSignUpQRCode == null)
mSignUpQRCode = new QrBitmap(getSignUpMnemonic(), Color.WHITE).getQRCode();
return mSignUpQRCode;
}
public void addBalanceObserver(final int subAccount, final Observer o) {
mBalanceObservables.get(subAccount).addObserver(o);
}
public void deleteBalanceObserver(final int subAccount, final Observer o) {
mBalanceObservables.get(subAccount).deleteObserver(o);
}
public void addNewTxObserver(final Observer o) {
mNewTxObservable.addObserver(o);
}
public void deleteNewTxObserver(final Observer o) {
mNewTxObservable.deleteObserver(o);
}
public void addVerifiedTxObserver(final Observer o) {
mVerifiedTxObservable.addObserver(o);
}
public void deleteVerifiedTxObserver(final Observer o) {
mVerifiedTxObservable.deleteObserver(o);
}
public void addTwoFactorObserver(final Observer o) {
mTwoFactorConfigObservable.addObserver(o);
}
public void deleteTwoFactorObserver(final Observer o) {
mTwoFactorConfigObservable.deleteObserver(o);
}
public void notifyObservers(final Sha256Hash txHash) {
// FIXME: later spent outputs can be purged
mSPV.addUtxoToValues(txHash, true /* updateVerified */);
mVerifiedTxObservable.doNotify();
}
public Coin getCoinBalance(final int subAccount) {
return mCoinBalances.get(subAccount);
}
public String getFiatBalance(final int subAccount) {
if (!hasFiatRate())
return "N/A";
final Coin coinBalance = getCoinBalance(subAccount);
Fiat balance = getFiatRate().coinToFiat(coinBalance);
// Strip extra decimals (over 2 places) because that's what the old JS client does
balance = balance.subtract(balance.divideAndRemainder((long) Math.pow(10, Fiat.SMALLEST_UNIT_EXPONENT - 2))[1]);
return MonetaryFormat.FIAT.minDecimals(2).noCode().format(balance).toString();
}
public boolean hasFiatRate() {
return mFiatRate != null;
}
public ExchangeRate getFiatRate() {
final long rate = new BigDecimal(mFiatRate).movePointRight(Fiat.SMALLEST_UNIT_EXPONENT)
.toBigInteger().longValue();
return new ExchangeRate(Fiat.valueOf("???", rate));
}
public String getFiatCurrency() {
return mFiatCurrency;
}
public String getAssetSymbol() {
return mAssetSymbol;
}
public MonetaryFormat getAssetFormat() {
return mAssetFormat;
}
public String getFiatExchange() {
return mFiatExchange;
}
public ArrayList<Map<String, Object>> getSubaccounts() {
return mSubAccounts;
}
public boolean haveSubaccounts() {
return mSubAccounts != null && !mSubAccounts.isEmpty();
}
public Map<String, Object> findSubaccountByType(final Integer subAccount, final String type) {
if (haveSubaccounts())
for (final Map<String, Object> ret : mSubAccounts)
if (ret.get("pointer").equals(subAccount) &&
(type == null || ret.get("type").equals(type)))
return ret;
return null;
}
public Map<String, Object> findSubaccount(final Integer subAccount) {
return findSubaccountByType(subAccount, null);
}
public Map<?, ?> getTwoFactorConfig() {
return mTwoFactorConfig;
}
public ListenableFuture<Boolean> setUserConfig(final String key, final Object value, final boolean updateImmediately) {
return mClient.setUserConfig(ImmutableMap.of(key, value), updateImmediately);
}
public ListenableFuture<Boolean> setUserConfig(final Map<String, Object> values, final boolean updateImmediately) {
return mClient.setUserConfig(values, updateImmediately);
}
public ListenableFuture<Object> requestTwoFacCode(final String method, final String action, final Object data) {
return mClient.requestTwoFacCode(method, action, data);
}
public ListenableFuture<Map<?, ?>> prepareSweepSocial(final byte[] pubKey, final boolean useElectrum) {
return mClient.prepareSweepSocial(pubKey, useElectrum);
}
public ListenableFuture<Map<?, ?>> processBip70URL(final String url) {
return mClient.processBip70URL(url);
}
public ListenableFuture<PreparedTransaction> preparePayreq(final Coin amount, final Map<?, ?> data, final JSONMap privateData) {
preparePrivData(privateData);
return mClient.preparePayreq(amount, data, privateData);
}
public ListenableFuture<Boolean> initEnableTwoFac(final String type, final String details, final Map<?, ?> twoFacData) {
return mClient.initEnableTwoFac(type, details, twoFacData);
}
public ListenableFuture<Boolean> enableTwoFactor(final String type, final String code, final Object twoFacData) {
return Futures.transform(mClient.enableTwoFactor(type, code, twoFacData), new Function<Boolean, Boolean>() {
@Override
public Boolean apply(final Boolean input) {
getAvailableTwoFactorMethods();
return input;
}
});
}
public Boolean disableTwoFactor(final String type, final Map<String, String> twoFacData) throws Exception {
if (!mClient.disableTwoFactor(type, twoFacData))
return false;
mTwoFactorConfig = mClient.getTwoFactorConfigSync();
mTwoFactorConfigObservable.doNotify();
return true;
}
public void changeTxLimits(final long newValue, final Map<String, String> twoFacData) throws Exception {
mClient.changeTxLimits(newValue, twoFacData);
}
public List<String> getEnabledTwoFactorMethods() {
if (mTwoFactorConfig == null)
return null;
final String[] methods = getResources().getStringArray(R.array.twoFactorMethods);
final ArrayList<String> enabled = new ArrayList<>();
for (final String method : methods)
if (((Boolean) mTwoFactorConfig.get(method)))
enabled.add(method);
return enabled;
}
private static class GaObservable extends Observable {
public void doNotify() {
setChanged();
notifyObservers();
}
}
public int getCurrentBlock(){
return mCurrentBlock;
}
private void setCurrentBlock(final int newBlock){
// FIXME: In case a transaction list call races with a block
// notification, this could potentially go backwards. It can also
// go backwards following a reorg which is probably not handled well.
if (newBlock > mCurrentBlock)
mCurrentBlock = newBlock;
}
// FIXME: Operations should be atomic
public static class State extends Observable {
private ConnState mConnState;
private boolean mForcedLogout;
private boolean mForcedTimeout;
public State() {
mConnState = ConnState.OFFLINE;
setForcedLogout(false);
setForcedTimeout(false);
}
private void setForcedLogout(final boolean forcedLogout) { mForcedLogout = forcedLogout; }
private void setForcedTimeout(final boolean forcedTimeout) { mForcedTimeout = forcedTimeout; }
public boolean isForcedOff() { return mForcedLogout || mForcedTimeout; }
public boolean isLoggedIn() { return mConnState == ConnState.LOGGEDIN; }
public boolean isLoggedOrLoggingIn() {
return mConnState == ConnState.LOGGEDIN || mConnState == ConnState.LOGGINGIN;
}
public boolean isConnected() { return mConnState == ConnState.CONNECTED; }
public boolean isDisconnected() { return mConnState == ConnState.DISCONNECTED; }
public boolean isDisconnectedOrOffline() {
return mConnState == ConnState.DISCONNECTED || mConnState == ConnState.OFFLINE;
}
private void transitionTo(final ConnState newState) {
if (mConnState == newState)
return; // Nothing to do
if (newState == ConnState.OFFLINE) {
// Transition through disconnected before going offline
transitionTo(ConnState.DISCONNECTED);
}
mConnState = newState;
if (newState == ConnState.LOGGEDIN) {
setForcedLogout(false);
setForcedTimeout(false);
}
doNotify();
}
private void doNotify() {
setChanged();
// FIXME: Should pass a copy of ourselves
notifyObservers(this);
}
}
private final State mState = new State();
public boolean isForcedOff() { return mState.isForcedOff(); }
public boolean isLoggedIn() { return mState.isLoggedIn(); }
public boolean isLoggedOrLoggingIn() { return mState.isLoggedOrLoggingIn(); }
public boolean isConnected() { return mState.isConnected(); }
public void addConnectionObserver(final Observer o) { mState.addObserver(o); }
public void deleteConnectionObserver(final Observer o) { mState.deleteObserver(o); }
private ScheduledThreadPoolExecutor mTimerExecutor;
private BroadcastReceiver mNetConnectivityReceiver;
private ScheduledFuture<?> mDisconnectTimer;
private ScheduledFuture<?> mReconnectTimer;
private int mReconnectDelay;
private int mRefCount; // Number of non-paused activities using us
public void incRef() {
++mRefCount;
cancelDisconnect();
if (mState.isDisconnected())
reconnect();
}
public void decRef() {
if (BuildConfig.DEBUG && mRefCount <= 0)
throw new RuntimeException("Incorrect reference count");
if (--mRefCount == 0)
scheduleDisconnect();
}
private void cancelDisconnect() {
if (mDisconnectTimer != null && !mDisconnectTimer.isCancelled()) {
Log.d(TAG, "cancelDisconnect");
mDisconnectTimer.cancel(false);
}
}
private void scheduleDisconnect() {
final int delayMins = getAutoLogoutMinutes();
cancelDisconnect();
Log.d(TAG, "scheduleDisconnect in " + Integer.toString(delayMins) + " mins");
mDisconnectTimer = mTimerExecutor.schedule(new Runnable() {
public void run() {
Log.d(TAG, "scheduled disconnect");
mState.setForcedTimeout(true);
disconnect(false); // Calls transitionTo(DISCONNECTED)
}
}, delayMins, TimeUnit.MINUTES);
}
private void scheduleReconnect() {
final int RECONNECT_TIMEOUT = 6000;
final int RECONNECT_TIMEOUT_MAX = 50000;
if (mReconnectDelay < RECONNECT_TIMEOUT_MAX)
mReconnectDelay *= 1.2;
if (mReconnectDelay == 0)
mReconnectDelay = RECONNECT_TIMEOUT;
Log.d(TAG, "scheduleReconnect in " + Integer.toString(mReconnectDelay) + " ms");
if (mReconnectTimer != null && !mReconnectTimer.isCancelled()) {
Log.d(TAG, "cancelReconnect");
mReconnectTimer.cancel(false);
}
mReconnectTimer = mTimerExecutor.schedule(new Runnable() {
public void run() {
Log.d(TAG, "scheduled reconnect");
reconnect();
}
}, mReconnectDelay, TimeUnit.MILLISECONDS);
}
private void onNetConnectivityChanged() {
final NetworkInfo info = getNetworkInfo();
if (info == null) {
// No network connection, go offline until notified that its back
mState.transitionTo(ConnState.OFFLINE);
} else if (mState.isDisconnectedOrOffline()) {
// We have a network connection and are currently disconnected/offline:
// Move to disconnected and try to reconnect
mSPV.onNetConnectivityChangedAsync(info);
mState.transitionTo(ConnState.DISCONNECTED);
reconnect();
} else
mSPV.onNetConnectivityChangedAsync(info);
}
public NetworkInfo getNetworkInfo() {
final Context ctx = getApplicationContext();
final ConnectivityManager cm;
cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo ni = cm.getActiveNetworkInfo();
return ni != null && ni.isConnectedOrConnecting() ? ni : null;
}
public static Transaction buildTransaction(final String hex) {
return new Transaction(Network.NETWORK, Wally.hex_to_bytes(hex));
}
}