Java tutorial
// Copyright (C) 2013-2014 Bonsai Software, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. package com.bonsai.wallet32; import java.io.File; import java.io.InputStream; import java.io.IOException; import java.math.BigInteger; import java.text.DateFormat; import java.util.ArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.InvalidCipherTextException; import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.util.encoders.Hex; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; import com.google.bitcoin.core.AbstractWalletEventListener; import com.google.bitcoin.core.Address; import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.InsufficientMoneyException; import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.PeerGroup; import com.google.bitcoin.core.Sha256Hash; import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.TransactionBroadcaster; import com.google.bitcoin.core.TransactionConfidence; import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.core.TransactionInput; import com.google.bitcoin.core.TransactionOutPoint; import com.google.bitcoin.core.Utils; import com.google.bitcoin.core.Wallet; import com.google.bitcoin.core.Wallet.BalanceType; import com.google.bitcoin.core.WrongNetworkException; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.MnemonicCodeX; import com.google.bitcoin.script.Script; import com.google.bitcoin.wallet.WalletTransaction; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import hashengineering.groestlcoin.wallet32.R; public class WalletService extends Service implements OnSharedPreferenceChangeListener { public static boolean mIsRunning = false; private static Logger mLogger = LoggerFactory.getLogger(WalletService.class); private ScheduledExecutorService mTimeoutWorker; private ScheduledFuture<?> mBackgroundTimeout = null; private WalletApplication mApp; public enum State { SETUP, // CTOR WALLET_SETUP, // Setting up wallet app kit. KEYS_ADD, // Adding keys. PEERING, // Connecting to peers. SYNCING, // Many times from sync progress. READY, SHUTDOWN, ERROR } public enum SyncState { CREATED, // First scan after creation. RESTORE, // Scanning to restore. STARTUP, // Catching up on startup. RESCAN, // Rescanning blockchain. RERESCAN, // Needed to rescan due to margin. SYNCHRONIZED // We were synchronized. } private int NOTIFICATION = R.string.wallet_service_started; private NotificationManager mNM; private LocalBroadcastManager mLBM; private final IBinder mBinder = new WalletServiceBinder(); private State mState; private SyncState mSyncState; private MyWalletAppKit mKit; private NetworkParameters mParams; private SetupWalletTask mTask; private Context mContext; private Resources mRes; private SharedPreferences mPrefs; private double mPercentDone = 0.0; private int mBlocksToGo; private Date mScanDate; private long mMsecsLeft; private KeyCrypter mKeyCrypter; private KeyParameter mAesKey; private HDWallet mHDWallet = null; private RateUpdater mRateUpdater; private BigInteger mBalanceAvailable; private BigInteger mBalanceEstimated; private WakeLock mWakeLock; private volatile int mNoteId = 0; private static final String mFilePrefix = "wallet32"; private MyDownloadListener mkDownloadListener() { return new MyDownloadListener() { protected void progress(double pct, int blocksToGo, Date date, long msecsLeft) { Date cmplDate = new Date(System.currentTimeMillis() + msecsLeft); mLogger.info(String.format("CHAIN DOWNLOAD %d%% DONE WITH %d BLOCKS TO GO, " + "COMPLETE AT %s", (int) pct, blocksToGo, DateFormat.getDateTimeInstance().format(cmplDate))); mBlocksToGo = blocksToGo; mScanDate = date; mMsecsLeft = msecsLeft; if (mPercentDone != pct) { mPercentDone = pct; setState(State.SYNCING); } } }; } private AbstractWalletEventListener mWalletListener = new AbstractWalletEventListener() { @Override public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { BigInteger amt = newBalance.subtract(prevBalance); final long amount = amt.longValue(); WalletApplication app = (WalletApplication) getApplicationContext(); final BTCFmt btcfmt = app.getBTCFmt(); // Change coins will be part of a balance transaction // that is negative in value ... skip them ... if (amount < 0) return; // We allocate a new notification id for each receive. // We use it on both the receive and confirm so it // will replace the receive note with the confirm ... final int noteId = ++mNoteId; mLogger.info(String.format("showing notification receive %d", amount)); showEventNotification(noteId, R.drawable.ic_note_bc_green_lt, mRes.getString(R.string.wallet_service_note_rcvd_title, btcfmt.unitStr()), mRes.getString(R.string.wallet_service_note_rcvd_msg, btcfmt.format(amount), btcfmt.unitStr())); final TransactionConfidence txconf = tx.getConfidence(); final TransactionConfidence.Listener listener = new TransactionConfidence.Listener() { @Override public void onConfidenceChanged(Transaction tx, ChangeReason reason) { // Wait until it's not pending anymore. if (tx.isPending()) return; ConfidenceType ct = tx.getConfidence().getConfidenceType(); if (ct == ConfidenceType.BUILDING) { mLogger.info(String.format("receive %d confirm", amount)); // Notify confirmed. showEventNotification(noteId, R.drawable.ic_note_bc_green, mRes.getString(R.string.wallet_service_note_rcnf_title, btcfmt.unitStr()), mRes.getString(R.string.wallet_service_note_rcnf_msg, btcfmt.format(amount), btcfmt.unitStr())); } else if (ct == ConfidenceType.DEAD) { mLogger.info(String.format("receive %d dead", amount)); // Notify dead. showEventNotification(noteId, R.drawable.ic_note_bc_gray, mRes.getString(R.string.wallet_service_note_rdead_title, btcfmt.unitStr()), mRes.getString(R.string.wallet_service_note_rdead_msg, btcfmt.format(amount), btcfmt.unitStr())); } else { mLogger.info(String.format("receive %d unknown", amount)); } // We're all done listening ... txconf.removeEventListener(this); } }; txconf.addEventListener(listener); } @Override public void onCoinsSent(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { BigInteger amt = prevBalance.subtract(newBalance); final long amount = amt.longValue(); WalletApplication app = (WalletApplication) getApplicationContext(); final BTCFmt btcfmt = app.getBTCFmt(); // We allocate a new notification id for each receive. // We use it on both the receive and confirm so it // will replace the receive note with the confirm ... final int noteId = ++mNoteId; mLogger.info(String.format("showing notification send %d", amount)); showEventNotification(noteId, R.drawable.ic_note_bc_red_lt, mRes.getString(R.string.wallet_service_note_sent_title, btcfmt.unitStr()), mRes.getString(R.string.wallet_service_note_sent_msg, btcfmt.format(amount), btcfmt.unitStr())); final TransactionConfidence txconf = tx.getConfidence(); final TransactionConfidence.Listener listener = new TransactionConfidence.Listener() { @Override public void onConfidenceChanged(Transaction tx, ChangeReason reason) { // Wait until it's not pending anymore. if (tx.isPending()) return; ConfidenceType ct = tx.getConfidence().getConfidenceType(); if (ct == ConfidenceType.BUILDING) { mLogger.info(String.format("send %d confirm", amount)); // Show no longer pending. showEventNotification(noteId, R.drawable.ic_note_bc_red, mRes.getString(R.string.wallet_service_note_scnf_title, btcfmt.unitStr()), mRes.getString(R.string.wallet_service_note_scnf_msg, btcfmt.format(amount), btcfmt.unitStr())); } else if (ct == ConfidenceType.DEAD) { mLogger.info(String.format("send %d dead", amount)); // Notify dead. showEventNotification(noteId, R.drawable.ic_note_bc_gray, mRes.getString(R.string.wallet_service_note_sdead_title, btcfmt.unitStr()), mRes.getString(R.string.wallet_service_note_sdead_msg, btcfmt.format(amount), btcfmt.unitStr())); } else { mLogger.info(String.format("send %d unknown", amount)); } // We're all done listening ... txconf.removeEventListener(this); } }; txconf.addEventListener(listener); } @Override public void onWalletChanged(Wallet wallet) { // Compute balances and transaction counts. Iterable<WalletTransaction> iwt = mKit.wallet().getWalletTransactions(); mHDWallet.applyAllTransactions(iwt); // Check to make sure we have sufficient margins. int maxExtended = mHDWallet.ensureMargins(mKit.wallet()); // Persist the new state. mHDWallet.persist(mApp); Intent intent = new Intent("wallet-state-changed"); mLBM.sendBroadcast(intent); if (maxExtended > HDChain.maxSafeExtend()) { mLogger.info(String.format("%d addresses added, rescanning", maxExtended)); //rescanBlockchain(HDAddress.EPOCH); rescanBlockchain(mKit.getCreationTime() - 24 * 7 * 3600); } } }; public void shutdown() { mLogger.info("shutdown"); mState = State.SHUTDOWN; try { if (mKit != null) mKit.shutDown(); } catch (Exception ex) { mLogger.error("Trouble during shutdown: " + ex.toString()); } } // Called when UI activities all pause, terminates the service // unless canceled. // public void startBackgroundTimeout() { // Cancel any pre-existing timeout. cancelBackgroundTimeout(); Runnable task = new Runnable() { public void run() { mLogger.info("background timeout"); mApp.doExit(); } }; String delaystr = mPrefs.getString(SettingsActivity.KEY_BACKGROUND_TIMEOUT, "600"); long delay; try { delay = Long.parseLong(delaystr); } catch (NumberFormatException ex) { throw new RuntimeException(ex.toString()); // Shouldn't happen. } if (delay != -1) { mBackgroundTimeout = mTimeoutWorker.schedule(task, delay, TimeUnit.SECONDS); mLogger.info(String.format("background timeout scheduled in %d seconds", delay)); } } // Called when UI activites resume. // public void cancelBackgroundTimeout() { if (mBackgroundTimeout != null) { mBackgroundTimeout.cancel(false); mBackgroundTimeout = null; mLogger.info("background timeout canceled"); } } private class SetupWalletTask extends AsyncTask<Long, Void, Integer> { @Override protected void onPreExecute() { mWakeLock.acquire(); mLogger.info("wakelock acquired"); } @Override protected Integer doInBackground(Long... params) { // scanTime 0 : full rescan // scanTime t : scan from time t final Long scanTime = params[0]; setState(State.WALLET_SETUP); mLogger.info("setting up wallet, scanTime=" + scanTime.toString()); mLogger.info("getting network parameters"); mParams = Constants.getNetworkParameters(getApplicationContext()); // Try to restore existing wallet. mHDWallet = null; try { mHDWallet = HDWallet.restore(mApp, mParams, mKeyCrypter, mAesKey); } catch (InvalidCipherTextException ex) { mLogger.error("wallet restore failed 1: " + ex.toString()); } catch (IOException ex) { mLogger.error("wallet restore failed 2: " + ex.toString()); } catch (RuntimeException ex) { mLogger.error("wallet restore failed 3: " + ex.toString()); ex.printStackTrace(); if (ex.getCause() != null) { mLogger.error("exception cause: " + ex.getCause().toString()); } else mLogger.error("exception has no cause"); } if (mHDWallet == null) { mLogger.error("WalletService started with bad HDWallet"); System.exit(0); } mLogger.info("creating new wallet app kit"); // Checkpointing fails on full rescan because the earliest // create time is earlier than the genesis block time. // InputStream chkpntis = null; if (scanTime != 0) { try { chkpntis = getAssets().open(Constants.CHECKPOINTS_FILENAME); } catch (IOException e) { chkpntis = null; } } mKit = new MyWalletAppKit(mParams, mApp.getWalletDir(), mApp.getWalletPrefix(), mKeyCrypter, scanTime) { @Override protected void onSetupCompleted() { mLogger.info("adding keys"); setState(WalletService.State.KEYS_ADD); // Add all the existing keys, they'll be // ignored if they are already in the // WalletAppKit. // ArrayList<ECKey> keys = new ArrayList<ECKey>(); mHDWallet.gatherAllKeys(HDAddress.EPOCH, keys); mLogger.info(String.format("adding %d keys", keys.size())); wallet().addKeys(keys); // Do we have enough margin on all our chains? // Add keys to chains which don't have enough // unused addresses at the end. // mHDWallet.ensureMargins(wallet()); peerGroup().setFastCatchupTimeSecs( (scanTime == 0 ? mParams.getGenesisBlock().getTimeSeconds() : scanTime)); if (mPrefs.getBoolean("pref_reduceBloomFalsePositives", false)) { mLogger.info("reducing bloom false positives"); peerGroup().setBloomFilterFalsePositiveRate(0.000001); } // We don't need to check for HDChain.maxSafeExtend() // here because we are about to scan anyway. // We'll check again after the scan ... // Now we're peering. setState(WalletService.State.PEERING); } }; mKit.setDownloadListener(mkDownloadListener()); if (chkpntis != null) mKit.setCheckpoints(chkpntis); setState(State.WALLET_SETUP); mLogger.info("waiting for blockchain setup"); // Download the block chain and wait until it's done. mKit.startAndWait(); mLogger.info("blockchain setup finished, state = " + getStateString()); // Bail if we're being shutdown ... if (mState == State.SHUTDOWN) { mHDWallet.persist(mApp); return null; } mBalanceAvailable = mKit.wallet().getBalance(BalanceType.AVAILABLE); mBalanceEstimated = mKit.wallet().getBalance(BalanceType.ESTIMATED); mLogger.info("avail balance = " + mBalanceAvailable.toString()); mLogger.info("estim balance = " + mBalanceEstimated.toString()); // Compute balances and transaction counts. Iterable<WalletTransaction> iwt = mKit.wallet().getWalletTransactions(); mHDWallet.applyAllTransactions(iwt); // Check the margins again, since transactions may have arrived. int maxExtended = mHDWallet.ensureMargins(mKit.wallet()); // Persist the new state. mHDWallet.persist(mApp); // Listen for future wallet changes. mKit.wallet().addEventListener(mWalletListener); setState(State.READY); // This may be temporary ... return maxExtended; } @Override protected void onPostExecute(Integer maxExtended) { // Restore default (might have been reduced ...) mLogger.info("setting bloom filter false positives to default"); mKit.peerGroup().setBloomFilterFalsePositiveRate(PeerGroup.DEFAULT_BLOOM_FILTER_FP_RATE); mWakeLock.release(); mLogger.info("wakelock released"); // Do we need another rescan? if (maxExtended > HDChain.maxSafeExtend()) { mLogger.info(String.format("rescan extended by %d, rescanning", maxExtended)); //rescanBlockchain(HDAddress.EPOCH); rescanBlockchain(mKit.getCreationTime() - 24 * 7 * 3600); } else { mLogger.info("synchronized"); setSyncState(SyncState.SYNCHRONIZED); } } } public WalletService() { mState = State.SETUP; } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mLBM = LocalBroadcastManager.getInstance(this); mLogger.info("WalletService created"); mApp = (WalletApplication) getApplicationContext(); mContext = getApplicationContext(); mRes = mContext.getResources(); mTimeoutWorker = Executors.newSingleThreadScheduledExecutor(); final String lockName = getPackageName() + " blockchain sync"; final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); String fiatRateSource = mPrefs.getString(SettingsActivity.KEY_FIAT_RATE_SOURCE, ""); setFiatRateSource(fiatRateSource); // Register for future preference changes. mPrefs.registerOnSharedPreferenceChangeListener(this); // Register with the WalletApplication. mApp.setWalletService(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // Establish our SyncState mSyncState = SyncState.STARTUP; if (intent != null) { Bundle bundle = intent.getExtras(); String syncStateStr = bundle.getString("SyncState"); if (syncStateStr != null) mSyncState = syncStateStr.equals("CREATED") ? SyncState.CREATED : syncStateStr.equals("RESTORE") ? SyncState.RESTORE : syncStateStr.equals("STARTUP") ? SyncState.STARTUP : syncStateStr.equals("RESCAN") ? SyncState.RESCAN : syncStateStr.equals("RERESCAN") ? SyncState.RERESCAN : SyncState.STARTUP; } mKeyCrypter = mApp.mKeyCrypter; mAesKey = mApp.mAesKey; // Set any new key's creation time to now. long now = Utils.now().getTime() / 1000; mTask = new SetupWalletTask(); mTask.execute(now); mLogger.info("WalletService started"); showStatusNotification(); mIsRunning = true; return Service.START_STICKY; } @Override public void onDestroy() { mLogger.info("onDestroy called"); mIsRunning = false; // FIXME - Where does this go? Anywhere? // stopForeground(true); mNM.cancel(NOTIFICATION); } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(SettingsActivity.KEY_FIAT_RATE_SOURCE)) { String fiatRateSource = mPrefs.getString(SettingsActivity.KEY_FIAT_RATE_SOURCE, ""); setFiatRateSource(fiatRateSource); } } // Show a notification while this service is running. // private void showStatusNotification() { CharSequence started_txt = getText(R.string.wallet_service_started); CharSequence info_txt = getText(R.string.wallet_service_info); Notification note = new Notification(R.drawable.ic_stat_notify, started_txt, System.currentTimeMillis()); Intent intent = new Intent(this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0); // Set the info for the views that show in the notification panel. note.setLatestEventInfo(this, getText(R.string.wallet_service_label), info_txt, contentIntent); note.flags |= Notification.FLAG_NO_CLEAR; startForeground(NOTIFICATION, note); } private void showEventNotification(int noteId, int icon, String title, String msg) { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).setSmallIcon(icon) .setContentTitle(title).setContentText(msg).setAutoCancel(true).setDefaults( Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE); // Creates an explicit intent for an Activity in your app Intent intent = new Intent(this, ViewTransactionsActivity.class); // The stack builder object will contain an artificial back // stack for the started Activity. This ensures that // navigating backward from the Activity leads out of your // application to the Home screen. TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); // Adds the back stack for the Intent (but not the Intent itself) stackBuilder.addParentStack(ViewTransactionsActivity.class); // Adds the Intent that starts the Activity to the top of the stack stackBuilder.addNextIntent(intent); PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(resultPendingIntent); mNM.notify(noteId, mBuilder.build()); } public void persist() { mHDWallet.persist(mApp); } public byte[] getWalletSeed() { return mHDWallet == null ? null : mHDWallet.getWalletSeed(); } public String getPairingCode() { JSONObject obj = mHDWallet.dumps(true); return obj.toString(); } public String getFormatVersionString() { return mHDWallet.getFormatVersionString(); } public HDWallet.HDStructVersion getHDStructVersion() { return mHDWallet.getHDStructVersion(); } public MnemonicCodeX.Version getBIP39Version() { return mHDWallet.getBIP39Version(); } public void changePasscode(KeyParameter oldAesKey, KeyCrypter keyCrypter, KeyParameter aesKey) { mLogger.info("changePasscode starting"); // Change the parameters on our HDWallet. mHDWallet.setPersistCrypter(keyCrypter, aesKey); mHDWallet.persist(mApp); mLogger.info("persisted HD wallet"); // Decrypt the wallet with the old key. mKit.wallet().decrypt(oldAesKey); mLogger.info("decrypted base wallet"); // Encrypt the wallet using the new key. mKit.wallet().encrypt(keyCrypter, aesKey); mLogger.info("reencrypted base wallet"); } private void setFiatRateSource(String src) { if (mRateUpdater != null) { mRateUpdater.stopUpdater(); mRateUpdater = null; } if (src.equals("WINKDEXUSD")) { mLogger.info("Switching to WinkDex USD"); mRateUpdater = new WinkDexRateUpdater(getApplicationContext()); } else if (src.equals("COINDESKUSD")) { mLogger.info("Switching to CoinDesk BPI USD"); mRateUpdater = new CoinDeskRateUpdater(getApplicationContext()); } else if (src.equals("BITSTAMPUSD")) { mLogger.info("Switching to BitStamp USD"); mRateUpdater = new BitStampRateUpdater(getApplicationContext()); } else { mLogger.warn("Unknown fiat rate source " + src); return; } mRateUpdater.startUpdater(); } public void addAccount() { mLogger.info("add account"); // Make sure we are in a good state for this. if (mState != State.READY) { mLogger.warn("can't add an account until the wallet is ready"); return; } mHDWallet.addAccount(); mHDWallet.ensureMargins(mKit.wallet()); // Set the new keys creation time to now. long now = Utils.now().getTime() / 1000; // Adding all the keys is overkill, but it is simpler for now. ArrayList<ECKey> keys = new ArrayList<ECKey>(); mHDWallet.gatherAllKeys(now, keys); mLogger.info(String.format("adding %d keys", keys.size())); mKit.wallet().addKeys(keys); mHDWallet.persist(mApp); } public void rescanBlockchain(long rescanTime) { mLogger.info(String.format("RESCANNING from %d", rescanTime)); if (rescanTime == 0) rescanTime = mKit.getCreationTime(); // Make sure we are in a good state for this. if (mState != State.READY) { mLogger.warn("can't rescan until the wallet is ready"); return; } switch (mSyncState) { case SYNCHRONIZED: mSyncState = SyncState.RESCAN; break; default: mSyncState = SyncState.RERESCAN; break; } // Remove our wallet event listener. mKit.wallet().removeEventListener(mWalletListener); // Persist and remove our HDWallet. // // NOTE - It's best not to clear the balances here. When the // transactions are filling in on the transactions screen it's // disturbing to see negative historical balances. They'll // get completely refigured when the sync is done anyway ... // mHDWallet.persist(mApp); mHDWallet = null; mLogger.info("resetting wallet state"); mKit.wallet().clearTransactions(0); mKit.wallet().setLastBlockSeenHeight(-1); // magic value mKit.wallet().setLastBlockSeenHash(null); mLogger.info("shutting kit down"); try { mKit.shutDown(); mKit = null; } catch (Exception ex) { mLogger.error("kit shutdown failed: " + ex.toString()); return; } File dir = mApp.getWalletDir(); String spvpath = mApp.getWalletPrefix() + ".spvchain"; mLogger.info("removing spvchain file " + dir + spvpath); File chainFile = new File(dir, spvpath); if (!chainFile.delete()) mLogger.error("delete of spvchain file failed"); mLogger.info("restarting wallet"); mKeyCrypter = mApp.mKeyCrypter; mAesKey = mApp.mAesKey; setState(State.SETUP); mTask = new SetupWalletTask(); mTask.execute(rescanTime); } public void setSyncState(SyncState syncState) { mSyncState = syncState; } public State getState() { return mState; } public SyncState getSyncState() { return mSyncState; } public double getPercentDone() { return mPercentDone; } public int getBlocksToGo() { return mBlocksToGo; } public Date getScanDate() { return mScanDate; } public long getMsecsLeft() { return mMsecsLeft; } public String getStateString() { switch (mState) { case SETUP: return mRes.getString(R.string.network_status_setup); case WALLET_SETUP: return mRes.getString(R.string.network_status_wallet); case KEYS_ADD: return mRes.getString(R.string.network_status_keysadd); case PEERING: return mRes.getString(R.string.network_status_peering); case SYNCING: return mRes.getString(R.string.network_status_sync, (int) mPercentDone); case READY: return mRes.getString(R.string.network_status_ready); case SHUTDOWN: return mRes.getString(R.string.network_status_shutdown); case ERROR: return mRes.getString(R.string.network_status_error); default: return mRes.getString(R.string.network_status_unknown); } } public class WalletServiceBinder extends Binder { WalletService getService() { return WalletService.this; } } public NetworkParameters getParams() { return mParams; } public double getRate() { return mRateUpdater == null ? 0.0 : mRateUpdater.getRate(); } public String getCode() { return mRateUpdater == null ? "???" : mRateUpdater.getCode(); } static public long getDefaultFee() { final BigInteger dmtf = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE; return dmtf.longValue(); } public List<HDAccount> getAccounts() { if (mHDWallet == null) return null; return mHDWallet.getAccounts(); } public HDAccount getAccount(int accountId) { if (mHDWallet == null) return null; return mHDWallet.getAccount(accountId); } public List<Balance> getBalances() { if (mHDWallet == null) return null; List<Balance> balances = new LinkedList<Balance>(); mHDWallet.getBalances(balances); return balances; } public Iterable<WalletTransaction> getTransactions() { if (mHDWallet == null) return null; return mKit.wallet().getWalletTransactions(); } public Transaction getTransaction(String hashstr) { Sha256Hash hash = new Sha256Hash(hashstr); return mKit.wallet().getTransaction(hash); } public Address nextReceiveAddress(int acctnum) { return mHDWallet.nextReceiveAddress(acctnum); } public HDAddressDescription findAddress(Address addr) { return mHDWallet.findAddress(addr); } public static class AmountAndFee { public long mAmount; public long mFee; public AmountAndFee(long amt, long fee) { mAmount = amt; mFee = fee; } } public AmountAndFee useAll(int acctnum, boolean spendUnconfirmed) throws InsufficientMoneyException { return mHDWallet.useAll(mKit.wallet(), acctnum, spendUnconfirmed); } public long computeRecommendedFee(int acctnum, long amount, boolean spendUnconfirmed) throws IllegalArgumentException, InsufficientMoneyException { mLogger.info("computeRecommendedFee starting"); long fee = mHDWallet.computeRecommendedFee(mKit.wallet(), acctnum, amount, spendUnconfirmed); mLogger.info("computeRecommendedFee finished"); return fee; } public void sendCoinsFromAccount(int acctnum, String address, long amount, long fee, boolean spendUnconfirmed) throws RuntimeException { if (mHDWallet == null) return; try { Address dest = new Address(mParams, address); mLogger.info( String.format("send coins: acct=%d, dest=%s, val=%d, fee=%d", acctnum, address, amount, fee)); mHDWallet.sendAccountCoins(mKit.wallet(), acctnum, dest, amount, fee, spendUnconfirmed); } catch (WrongNetworkException ex) { String msg = "Address for wrong network: " + ex.getMessage(); throw new RuntimeException(msg); } catch (AddressFormatException ex) { String msg = "Malformed bitcoin address: " + ex.getMessage(); throw new RuntimeException(msg); } } public long amountForAccount(WalletTransaction wtx, int acctnum) { return mHDWallet.amountForAccount(wtx, acctnum); } public long balanceForAccount(int acctnum) { return mHDWallet.balanceForAccount(acctnum); } public long availableForAccount(int acctnum) { return mHDWallet.availableForAccount(acctnum); } private void setState(State newstate) { // SHUTDOWN is final ... if (mState == State.SHUTDOWN) return; mLogger.info("setState " + getStateString()); mState = newstate; sendStateChanged(); } private void sendStateChanged() { Intent intent = new Intent("wallet-state-changed"); mLBM.sendBroadcast(intent); } public void sweepKey(ECKey key, long fee, int accountId, JSONArray outputs) { mLogger.info("sweepKey starting"); mLogger.info("key addr " + key.toAddress(mParams).toString()); Transaction tx = new Transaction(mParams); long balance = 0; ArrayList<Script> scripts = new ArrayList<Script>(); try { for (int ii = 0; ii < outputs.length(); ++ii) { JSONObject output; output = outputs.getJSONObject(ii); String tx_hash = output.getString("tx_hash"); int tx_output_n = output.getInt("tx_ouput_n"); //this typo is necessary for Groestlcoin String script = output.getString("script"); // Reverse byte order, create hash. Sha256Hash hash = new Sha256Hash(/*WalletUtil.msgHexToBytes(*/tx_hash/*)*/); tx.addInput(new TransactionInput(mParams, tx, new byte[] {}, new TransactionOutPoint(mParams, tx_output_n, hash))); scripts.add(new Script(Hex.decode(script))); balance += output.getLong("value"); } } catch (JSONException e) { e.printStackTrace(); throw new RuntimeException("trouble parsing unspent outputs"); } // Compute balance - fee. long amount = balance - fee; mLogger.info(String.format("sweeping %d", amount)); // Figure out the destination address. Address to = mHDWallet.nextReceiveAddress(accountId); mLogger.info("sweeping to " + to.toString()); // Add output. tx.addOutput(BigInteger.valueOf(amount), to); WalletUtil.signTransactionInputs(tx, Transaction.SigHash.ALL, key, scripts); mLogger.info("tx bytes: " + new String(Hex.encode(tx.bitcoinSerialize()))); // mKit.peerGroup().broadcastTransaction(tx); broadcastTransaction(mKit.peerGroup(), tx); mLogger.info("sweepKey finished"); } public void broadcastTransaction(final TransactionBroadcaster broadcaster, final Transaction tx) { new Thread() { @Override public void run() { // Handle the future results just for logging. try { Futures.addCallback(broadcaster.broadcastTransaction(tx), new FutureCallback<Transaction>() { @Override public void onSuccess(Transaction transaction) { mLogger.info("Successfully broadcast key rotation tx: {}", transaction); } @Override public void onFailure(Throwable throwable) { mLogger.error("Failed to broadcast key rotation tx", throwable); } }); } catch (Exception e) { mLogger.error("Failed to broadcast rekey tx", e); } } }.start(); } } // Local Variables: // mode: java // c-basic-offset: 4 // tab-width: 4 // End: