package com.bitcoinlabs.android; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.math.BigInteger; import com.google.bitcoin.core.Address; import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.Utils; import android.util.Log; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteOpenHelper; import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import java.util.HashMap; import java.math.BigInteger; import com.google.bitcoin.core.Transaction; import com.google.bitcoin.core.TransactionStandaloneEncoder; import com.google.bitcoin.core.NetworkParameters; public class WalletOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "keys"; private static final int DATABASE_VERSION = 3; public static final String KEY = "key"; public static final String ADDRESS = "address58"; private static final String HASH = "hash"; private static final String N = "n"; private static final String SATOSHIS = "satoshis"; private static final String SPENT = "spent"; WalletOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { createTableKeys(db); createTableOutpoints(db); } @Override public void onUpgrade (SQLiteDatabase db, int oldVersion, int newVersion){ if (oldVersion <= 2) { db.execSQL("DROP TABLE outpoints;"); createTableOutpoints(db); } } private void createTableKeys(SQLiteDatabase db) { db.execSQL( "CREATE TABLE keys (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + ADDRESS + " TEXT," + KEY + " BLOB);"); } private void createTableOutpoints(SQLiteDatabase db) { db.execSQL( "CREATE TABLE outpoints (" + HASH + " BLOB," + ADDRESS + " TEXT," + N + " INTEGER," + SATOSHIS + " INTEGER," + SPENT + " INTEGER DEFAULT 0," + "PRIMARY KEY (" + HASH + "," + N + "));"); } public Address newKey() { ECKey key = new ECKey(); Address address = addKey(key); return address; } public Address addKey(ECKey key) { SQLiteDatabase db = getWritableDatabase(); Address address = key.toAddress(NetworkParameters.prodNet()); db.execSQL( "INSERT INTO keys ('address58', 'key') VALUES (?, ?)", new Object[] { address.toString(), key.getPrivKey().toByteArray()} ); db.close(); return address; } public ECKey getKey(String address58) { SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query("keys", new String[]{KEY}, "address58 = ?", new String[]{address58}, null, null, null, "1"); if (cursor.getCount() == 0) { return null; } else { cursor.moveToFirst(); return new ECKey(new BigInteger(cursor.getBlob(0))); } } public Cursor getAddresses() { SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query("keys", new String[]{ADDRESS}, null, null, null, null, null, "5"); return cursor; } public Address getUnusedAddress() { SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query("keys", new String[]{ADDRESS}, null, null, null, null, null, "1"); Address btcAddress; if (cursor.getCount() > 0) { cursor.moveToNext(); String addressString = cursor.getString(0); try { btcAddress = new Address(NetworkParameters.prodNet(), addressString); } catch (AddressFormatException e) { throw new RuntimeException("getUnusedAddress:" + e, e); } } else { btcAddress = newKey(); } cursor.close(); db.close(); return btcAddress; } public void add(OutpointsResponse outpointsResponse) { Collection<Outpoint> outpoints = outpointsResponse.getUnspent_outpoints(); if (outpoints != null && outpoints.size() > 0) { SQLiteDatabase db = getWritableDatabase(); for (Outpoint outpoint : outpoints) { outpoint.getAddress(); try { ContentValues values = new ContentValues(); values.put(HASH, Utils.hexStringToBytes(outpoint.getHash())); values.put(ADDRESS, outpoint.getAddress()); values.put(N, outpoint.getIndex()); values.put(SATOSHIS, outpoint.getSatoshis()); db.insertOrThrow("outpoints", null, values ); } catch (SQLiteConstraintException e) { //do nothing as we will assume we already have a record of this outpoint. //TODO verify that we have the right ADDRESS and SATOSHIS for this outpoint } } db.close(); } } public long getBalance() { long satoshis = 0; SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query("outpoints", new String[]{SATOSHIS}, "spent = 0", null, null, null, null); cursor.moveToFirst(); while (cursor.isAfterLast() == false) { satoshis += cursor.getLong(0); cursor.moveToNext(); } cursor.close(); return satoshis; } public Transaction createTransaction(long targetSatoshis, String destAddress, long feeSatoshis) { long satoshisGathered = 0; ArrayList<String> in_addresses = new ArrayList<String>(); ArrayList<byte[]> in_hashes = new ArrayList<byte[]>(); ArrayList<Integer> in_indexes = new ArrayList<Integer>(); HashMap<String, ECKey> address_key_map = new HashMap<String, ECKey>(); SQLiteDatabase db = getWritableDatabase(); // Read outpoints Cursor cursor = db.query("outpoints", new String[]{HASH, ADDRESS, N, SATOSHIS}, "spent = 0", null, null, null, null, null); cursor.moveToFirst(); while ((satoshisGathered < (targetSatoshis + feeSatoshis)) && (cursor.isAfterLast() == false)) { in_hashes.add(cursor.getBlob(0)); in_addresses.add(cursor.getString(1)); in_indexes.add(cursor.getInt(2)); satoshisGathered += cursor.getLong(3); cursor.moveToNext(); } cursor.close(); if (satoshisGathered < (targetSatoshis + feeSatoshis)) { return null; } // Read keys String whereClause = "(" + ADDRESS + " in ("; for (int i = 0; i < in_addresses.size(); i++) { if (i > 0) { whereClause += ", "; } whereClause += "'" + in_addresses.get(i) + "'"; } whereClause += "))"; cursor = db.query("keys", new String[]{ADDRESS, KEY}, whereClause, null, null, null, null, null); cursor.moveToFirst(); while (cursor.isAfterLast() == false) { address_key_map.put( cursor.getString(0), new ECKey(new BigInteger(cursor.getBlob(1)))); cursor.moveToNext(); } cursor.close(); // Create transaction Transaction tx; TransactionStandaloneEncoder tse = new TransactionStandaloneEncoder(NetworkParameters.prodNet()); for (int i = 0; i < in_addresses.size(); i++) { ECKey key = address_key_map.get(in_addresses.get(i)); int index = in_indexes.get(i).intValue(); byte[] hash = in_hashes.get(i); Log.i(getClass().getSimpleName()+"", "****** Input"); Log.i(getClass().getSimpleName()+"key address:", key.toAddress(NetworkParameters.prodNet()).toString()); Log.i(getClass().getSimpleName()+"", "" + index); Log.i(getClass().getSimpleName()+"", Utils.bytesToHexString(hash)); tse.addInput(key, index, hash); } try { Log.i(getClass().getSimpleName()+"", "****** Output"); Log.i(getClass().getSimpleName()+"", "" + targetSatoshis); Log.i(getClass().getSimpleName()+"", destAddress); tse.addOutput(new BigInteger("" + targetSatoshis), destAddress); if (satoshisGathered > (targetSatoshis + feeSatoshis)) { String changeAddress = getUnusedAddress().toString(); Log.i(getClass().getSimpleName()+"", "****** Output"); Log.i(getClass().getSimpleName()+"", "" + (satoshisGathered - (targetSatoshis + feeSatoshis))); Log.i(getClass().getSimpleName()+"", changeAddress); BigInteger changeSatoshis = new BigInteger("" + (satoshisGathered - (targetSatoshis + feeSatoshis))); tse.addOutput(changeSatoshis, changeAddress); } } catch (AddressFormatException e) { // TODO: handle better throw new RuntimeException("Invalid address!"); } tx = tse.createSignedTransaction(); // Spend outpoints db = getWritableDatabase(); for (int i = 0; i < in_hashes.size(); i++) { db.execSQL( "UPDATE outpoints SET spent = 1 WHERE ((" + HASH + " = ?) AND (" + N + " = ?));", new Object[]{in_hashes.get(i), in_indexes.get(i)}); } db.close(); return tx; } }