protected Collection<TransactionOutputEx> getSpendableOutputs() { Collection<TransactionOutputEx> list = _backing.getAllUnspentOutputs(); // Prune confirmed outputs for coinbase outputs that are not old enough // for spending. Also prune unconfirmed receiving coins except for change int blockChainHeight = getBlockChainHeight(); Iterator<TransactionOutputEx> it = list.iterator(); while (it.hasNext()) { TransactionOutputEx output = it.next(); if (output.isCoinBase) { int confirmations = blockChainHeight - output.height; if (confirmations < COINBASE_MIN_CONFIRMATIONS) { it.remove(); continue; } } // Unless we allow zero confirmation spending we prune all unconfirmed outputs sent from // foreign addresses if (!_allowZeroConfSpending) { if (output.height == -1 && !isFromMe(output.outPoint.hash)) { // Prune receiving coins that is not change sent to ourselves it.remove(); } } } return list; }
/** * Broadcast outgoing transactions. * * <p>This method should only be called from the wallet manager * * @return false if synchronization failed due to failed blockchain connection */ public synchronized boolean broadcastOutgoingTransactions() { checkNotArchived(); List<Sha256Hash> broadcastedIds = new LinkedList<Sha256Hash>(); Map<Sha256Hash, byte[]> transactions = _backing.getOutgoingTransactions(); for (byte[] rawTransaction : transactions.values()) { TransactionEx tex = TransactionEx.fromUnconfirmedTransaction(rawTransaction); BroadcastResult result = broadcastTransaction(TransactionEx.toTransaction(tex)); if (result == BroadcastResult.SUCCESS) { broadcastedIds.add(tex.txid); _backing.removeOutgoingTransaction(tex.txid); } else { if (result == BroadcastResult.REJECTED) { // invalid tx _backing.deleteTransaction(tex.txid); _backing.removeOutgoingTransaction(tex.txid); } else { // No connection --> retry next sync } } } if (!broadcastedIds.isEmpty()) { onTransactionsBroadcasted(broadcastedIds); } return true; }
@Override public TransactionDetails getTransactionDetails(Sha256Hash txid) { // Note that this method is not synchronized, and we might fetch the transaction history while // synchronizing // accounts. That should be ok as we write to the DB in a sane order. TransactionEx tex = _backing.getTransaction(txid); Transaction tx = TransactionEx.toTransaction(tex); if (tx == null) { throw new RuntimeException(); } List<TransactionDetails.Item> inputs = new ArrayList<TransactionDetails.Item>(tx.inputs.length); if (tx.isCoinbase()) { // We have a coinbase transaction. Create one input with the sum of the outputs as its value, // and make the address the null address long value = 0; for (TransactionOutput out : tx.outputs) { value += out.value; } inputs.add(new TransactionDetails.Item(Address.getNullAddress(_network), value, true)); } else { // Populate the inputs for (TransactionInput input : tx.inputs) { Sha256Hash parentHash = input.outPoint.hash; // Get the parent transaction TransactionOutputEx parentOutput = _backing.getParentTransactionOutput(input.outPoint); if (parentOutput == null) { // We never heard about the parent, skip continue; } // Determine the parent address Address parentAddress; ScriptOutput parentScript = ScriptOutput.fromScriptBytes(parentOutput.script); if (parentScript == null) { // Null address means we couldn't figure out the address, strange script parentAddress = Address.getNullAddress(_network); } else { parentAddress = parentScript.getAddress(_network); } inputs.add(new TransactionDetails.Item(parentAddress, parentOutput.value, false)); } } // Populate the outputs TransactionDetails.Item[] outputs = new TransactionDetails.Item[tx.outputs.length]; for (int i = 0; i < tx.outputs.length; i++) { Address address = tx.outputs[i].script.getAddress(_network); outputs[i] = new TransactionDetails.Item(address, tx.outputs[i].value, false); } return new TransactionDetails( txid, tex.height, tex.time, inputs.toArray(new TransactionDetails.Item[] {}), outputs); }
protected TransactionSummary transform( Transaction tx, int time, int height, int blockChainHeight) { long value = 0; Address destAddress = null; for (TransactionOutput output : tx.outputs) { if (isMine(output.script)) { value += output.value; } else { destAddress = output.script.getAddress(_network); } } if (tx.isCoinbase()) { // For coinbase transactions there is nothing to subtract } else { for (TransactionInput input : tx.inputs) { // find parent output TransactionOutputEx funding = _backing.getParentTransactionOutput(input.outPoint); if (funding == null) { _logger.logError("Unable to find parent output for: " + input.outPoint); continue; } if (isMine(funding)) { value -= funding.value; } } } int confirmations; if (height == -1) { confirmations = 0; } else { confirmations = Math.max(0, blockChainHeight - height + 1); } // only track a destinationAddress if it is an outgoing transaction (i.e. send money to someone) // to prevent the user that he tries to return money to an address he got bitcoin from. if (value >= 0) { destAddress = null; } boolean isQueuedOutgoing = _backing.isOutgoingTransaction(tx.getHash()); return new TransactionSummary( tx.getHash(), value, time, height, confirmations, isQueuedOutgoing, com.google.common.base.Optional.fromNullable(destAddress)); }
private void handleNewExternalTransactionsInt(Collection<TransactionEx> transactions) throws WapiException { // Transform and put into two arrays with matching indexes ArrayList<TransactionEx> texArray = new ArrayList<TransactionEx>(transactions.size()); ArrayList<Transaction> txArray = new ArrayList<Transaction>(transactions.size()); for (TransactionEx tex : transactions) { try { txArray.add(Transaction.fromByteReader(new ByteReader(tex.binary))); texArray.add(tex); } catch (TransactionParsingException e) { // We hit a transaction that we cannot parse. Log but otherwise ignore it _logger.logError("Received transaction that we cannot parse: " + tex.txid.toString()); continue; } } // Grab and handle parent transactions fetchStoreAndValidateParentOutputs(txArray); // Store transaction locally for (int i = 0; i < txArray.size(); i++) { _backing.putTransaction(texArray.get(i)); onNewTransaction(texArray.get(i), txArray.get(i)); } }
/** * Determine whether a transaction was sent from one of our own addresses. * * <p>This is a costly operation as we first have to lookup the transaction and then it's funding * outputs * * @param txid the ID of the transaction to investigate * @return true if one of the funding outputs were sent from one of our own addresses */ protected boolean isFromMe(Sha256Hash txid) { Transaction t = TransactionEx.toTransaction(_backing.getTransaction(txid)); if (t == null) { return false; } return isFromMe(t); }
public synchronized void queueTransaction(Transaction transaction) { // Store transaction in outgoing buffer, so we can broadcast it // later byte[] rawTransaction = transaction.toBytes(); _backing.putOutgoingTransaction(transaction.getHash(), rawTransaction); markTransactionAsSpent(transaction); }
@Override public synchronized boolean deleteTransaction(Sha256Hash transactionId) { TransactionEx tex = _backing.getTransaction(transactionId); if (tex == null) return false; Transaction tx = TransactionEx.toTransaction(tex); _backing.beginTransaction(); try { // See if any of the outputs are stored locally and remove them for (int i = 0; i < tx.outputs.length; i++) { TransactionOutput output = tx.outputs[i]; OutPoint outPoint = new OutPoint(tx.getHash(), i); TransactionOutputEx utxo = _backing.getUnspentOutput(outPoint); if (utxo != null) { _backing.deleteUnspentOutput(outPoint); } } // remove it from the backing _backing.deleteTransaction(transactionId); _backing.setTransactionSuccessful(); } finally { _backing.endTransaction(); } updateLocalBalance(); // will still need a new sync besides re-calculating return true; }
protected boolean monitorYoungTransactions() { Collection<TransactionEx> list = _backing.getYoungTransactions(5, getBlockChainHeight()); if (list.isEmpty()) { return true; } List<Sha256Hash> txids = new ArrayList<Sha256Hash>(list.size()); for (TransactionEx tex : list) { txids.add(tex.txid); } CheckTransactionsResponse result; try { result = _wapi.checkTransactions(new CheckTransactionsRequest(txids)).getResult(); } catch (WapiException e) { postEvent(Event.SERVER_CONNECTION_ERROR); _logger.logError("Server connection failed with error code: " + e.errorCode, e); // We failed to check transactions return false; } for (TransactionStatus t : result.transactions) { if (!t.found) { // We have a transaction locally that does not exist in the // blockchain. Must be a residue due to double-spend or malleability _backing.deleteTransaction(t.txid); continue; } TransactionEx tex = _backing.getTransaction(t.txid); Preconditions.checkNotNull(tex); if (tex.height != t.height || tex.time != t.time) { // The transaction got a new height or timestamp. There could be // several reasons for that. It got a new timestamp from the server, // it confirmed, or might also be a reorg. TransactionEx newTex = new TransactionEx(tex.txid, t.height, t.time, tex.binary); System.out.println("Replacing:\n" + tex.toString() + "\nWith:\n" + newTex.toString()); postEvent(Event.TRANSACTION_HISTORY_CHANGED); _backing.deleteTransaction(tex.txid); _backing.putTransaction(newTex); } } return true; }
/** * Determine whether a transaction was sent from one of our own addresses. * * <p>This is a costly operation as we have to lookup funding outputs of the transaction * * @param t the transaction to investigate * @return true iff one of the funding outputs were sent from one of our own addresses */ protected boolean isFromMe(Transaction t) { for (TransactionInput input : t.inputs) { TransactionOutputEx funding = _backing.getParentTransactionOutput(input.outPoint); if (funding == null || funding.isCoinBase) { continue; } ScriptOutput fundingScript = ScriptOutput.fromScriptBytes(funding.script); Address fundingAddress = fundingScript.getAddress(_network); if (isMine(fundingAddress)) { return true; } } return false; }
@Override public synchronized boolean cancelQueuedTransaction(Sha256Hash transaction) { Map<Sha256Hash, byte[]> outgoingTransactions = _backing.getOutgoingTransactions(); if (!outgoingTransactions.containsKey(transaction)) { return false; } Transaction tx; try { tx = Transaction.fromBytes(outgoingTransactions.get(transaction)); } catch (TransactionParsingException e) { return false; } _backing.beginTransaction(); try { // See if any of the outputs are stored locally and remove them for (int i = 0; i < tx.outputs.length; i++) { TransactionOutput output = tx.outputs[i]; OutPoint outPoint = new OutPoint(tx.getHash(), i); TransactionOutputEx utxo = _backing.getUnspentOutput(outPoint); if (utxo != null) { _backing.deleteUnspentOutput(outPoint); } } // Remove a queued transaction from our outgoing buffer _backing.removeOutgoingTransaction(transaction); // remove it from the backing _backing.deleteTransaction(transaction); _backing.setTransactionSuccessful(); } finally { _backing.endTransaction(); } // calc the new balance to remove the outgoing amount // the total balance will still be wrong, as we already deleted some UTXOs to build the queued // transaction // these will get restored after the next sync updateLocalBalance(); // markTransactionAsSpent(transaction); return true; }
private void markTransactionAsSpent(Transaction transaction) { _backing.beginTransaction(); try { // Remove inputs from unspent, marking them as spent for (TransactionInput input : transaction.inputs) { TransactionOutputEx parentOutput = _backing.getUnspentOutput(input.outPoint); if (parentOutput != null) { _backing.deleteUnspentOutput(input.outPoint); _backing.putParentTransactionOutput(parentOutput); } } // See if any of the outputs are for ourselves and store them as // unspent for (int i = 0; i < transaction.outputs.length; i++) { TransactionOutput output = transaction.outputs[i]; if (isMine(output.script)) { _backing.putUnspentOutput( new TransactionOutputEx( new OutPoint(transaction.getHash(), i), -1, output.value, output.script.getScriptBytes(), false)); } } // Store transaction locally, so we have it in our history and don't // need to fetch it in a minute _backing.putTransaction(TransactionEx.fromUnconfirmedTransaction(transaction)); _backing.setTransactionSuccessful(); } finally { _backing.endTransaction(); } // Tell account that we have a new transaction onNewTransaction(TransactionEx.fromUnconfirmedTransaction(transaction), transaction); // Calculate local balance cache. It has changed because we have done // some spending updateLocalBalance(); persistContextIfNecessary(); }
@Override public List<TransactionSummary> getTransactionHistory(int offset, int limit) { // Note that this method is not synchronized, and we might fetch the transaction history while // synchronizing // accounts. That should be ok as we write to the DB in a sane order. List<TransactionSummary> history = new ArrayList<TransactionSummary>(); checkNotArchived(); int blockChainHeight = getBlockChainHeight(); List<TransactionEx> list = _backing.getTransactionHistory(offset, limit); for (TransactionEx tex : list) { TransactionSummary item = transform(tex, blockChainHeight); if (item != null) { history.add(item); } } return history; }
@Override public List<TransactionOutputSummary> getUnspentTransactionOutputSummary() { // Note that this method is not synchronized, and we might fetch the transaction history while // synchronizing // accounts. That should be ok as we write to the DB in a sane order. // Get all unspent outputs for this account Collection<TransactionOutputEx> outputs = _backing.getAllUnspentOutputs(); // Transform it to a list of summaries List<TransactionOutputSummary> list = new ArrayList<TransactionOutputSummary>(); int blockChainHeight = getBlockChainHeight(); for (TransactionOutputEx output : outputs) { ScriptOutput script = ScriptOutput.fromScriptBytes(output.script); Address address; if (script == null) { address = Address.getNullAddress(_network); // This never happens as we have parsed this script before } else { address = script.getAddress(_network); } int confirmations; if (output.height == -1) { confirmations = 0; } else { confirmations = Math.max(0, blockChainHeight - output.height + 1); } TransactionOutputSummary summary = new TransactionOutputSummary( output.outPoint, output.value, output.height, confirmations, address); list.add(summary); } // Sort & return Collections.sort(list); return list; }
protected Balance calculateLocalBalance() { Collection<TransactionOutputEx> unspentOutputs = new HashSet<TransactionOutputEx>(_backing.getAllUnspentOutputs()); long confirmed = 0; long pendingChange = 0; long pendingSending = 0; long pendingReceiving = 0; // // Determine the value we are receiving and create a set of outpoints for fast lookup // Set<OutPoint> unspentOutPoints = new HashSet<OutPoint>(); for (TransactionOutputEx output : unspentOutputs) { if (output.height == -1) { if (isFromMe(output.outPoint.hash)) { pendingChange += output.value; } else { pendingReceiving += output.value; } } else { confirmed += output.value; } unspentOutPoints.add(output.outPoint); } // // Determine the value we are sending // // Get the current set of unconfirmed transactions List<Transaction> unconfirmed = new ArrayList<Transaction>(); for (TransactionEx tex : _backing.getUnconfirmedTransactions()) { try { Transaction t = Transaction.fromByteReader(new ByteReader(tex.binary)); unconfirmed.add(t); } catch (TransactionParsingException e) { // never happens, we have parsed it before } } for (Transaction t : unconfirmed) { // For each input figure out if WE are sending it by fetching the // parent transaction and looking at the address boolean weSend = false; for (TransactionInput input : t.inputs) { // Find the parent transaction if (input.outPoint.hash.equals(Sha256Hash.ZERO_HASH)) { continue; } TransactionOutputEx parent = _backing.getParentTransactionOutput(input.outPoint); if (parent == null) { _logger.logError("Unable to find parent transaction output: " + input.outPoint); continue; } TransactionOutput parentOutput = transform(parent); Address fundingAddress = parentOutput.script.getAddress(_network); if (isMine(fundingAddress)) { // One of our addresses are sending coins pendingSending += parentOutput.value; weSend = true; } } // Now look at the outputs and if it contains change for us, then subtract that from the // sending amount // if it is already spent in another transaction for (int i = 0; i < t.outputs.length; i++) { TransactionOutput output = t.outputs[i]; Address destination = output.script.getAddress(_network); if (weSend && isMine(destination)) { // The funds are sent from us to us OutPoint outPoint = new OutPoint(t.getHash(), i); if (!unspentOutPoints.contains(outPoint)) { // This output has been spent, subtract it from the amount sent pendingSending -= output.value; } } } } int blockHeight = getBlockChainHeight(); return new Balance( confirmed, pendingReceiving, pendingSending, pendingChange, System.currentTimeMillis(), blockHeight, true, _allowZeroConfSpending); }
@Override public TransactionSummary getTransactionSummary(Sha256Hash txid) { TransactionEx tx = _backing.getTransaction(txid); return transform(tx, tx.height); }
private void fetchStoreAndValidateParentOutputs(ArrayList<Transaction> transactions) throws WapiException { Map<Sha256Hash, TransactionEx> parentTransactions = new HashMap<Sha256Hash, TransactionEx>(); Map<OutPoint, TransactionOutputEx> parentOutputs = new HashMap<OutPoint, TransactionOutputEx>(); // Find list of parent outputs to fetch Collection<Sha256Hash> toFetch = new HashSet<Sha256Hash>(); for (Transaction t : transactions) { for (TransactionInput in : t.inputs) { if (in.outPoint.hash.equals(OutPoint.COINBASE_OUTPOINT.hash)) { // Coinbase input, so no parent continue; } TransactionOutputEx parentOutput = _backing.getParentTransactionOutput(in.outPoint); if (parentOutput != null) { // We already have the parent output, no need to fetch the entire // parent transaction parentOutputs.put(parentOutput.outPoint, parentOutput); continue; } TransactionEx parentTransaction = _backing.getTransaction(in.outPoint.hash); if (parentTransaction != null) { // We had the parent transaction in our own transactions, no need to // fetch it remotely parentTransactions.put(parentTransaction.txid, parentTransaction); } else { // Need to fetch it toFetch.add(in.outPoint.hash); } } } // Fetch missing parent transactions if (toFetch.size() > 0) { GetTransactionsResponse result = _wapi.getTransactions(new GetTransactionsRequest(Wapi.VERSION, toFetch)).getResult(); for (TransactionEx tx : result.transactions) { // Verify transaction hash. This is important as we don't want to // have a transaction output associated with an outpoint that // doesn't match. // This is the end users protection against a rogue server that lies // about the value of an output and makes you pay a large fee. Sha256Hash hash = HashUtils.doubleSha256(tx.binary).reverse(); if (hash.equals(tx.txid)) { parentTransactions.put(tx.txid, tx); } else { _logger.logError( "Failed to validate transaction hash from server. Expected: " + tx.txid + " Calculated: " + hash); throw new RuntimeException( "Failed to validate transaction hash from server. Expected: " + tx.txid + " Calculated: " + hash); } } } // We should now have all parent transactions or parent outputs. There is // a slight probability that one of them was not found due to double // spends and/or malleability and network latency etc. // Now figure out which parent outputs we need to persist List<TransactionOutputEx> toPersist = new LinkedList<TransactionOutputEx>(); for (Transaction t : transactions) { for (TransactionInput in : t.inputs) { if (in.outPoint.hash.equals(OutPoint.COINBASE_OUTPOINT.hash)) { // coinbase input, so no parent continue; } TransactionOutputEx parentOutput = parentOutputs.get(in.outPoint); if (parentOutput != null) { // We had it all along continue; } TransactionEx parentTex = parentTransactions.get(in.outPoint.hash); if (parentTex != null) { // Parent output not found, maybe we already have it parentOutput = TransactionEx.getTransactionOutput(parentTex, in.outPoint.index); toPersist.add(parentOutput); continue; } _logger.logError("Parent transaction not found: " + in.outPoint.hash); } } // Persist for (TransactionOutputEx output : toPersist) { _backing.putParentTransactionOutput(output); } }
protected boolean synchronizeUnspentOutputs(Collection<Address> addresses) { // Get the current unspent outputs as dictated by the block chain QueryUnspentOutputsResponse UnspentOutputResponse; try { UnspentOutputResponse = _wapi .queryUnspentOutputs(new QueryUnspentOutputsRequest(Wapi.VERSION, addresses)) .getResult(); } catch (WapiException e) { _logger.logError("Server connection failed with error code: " + e.errorCode, e); postEvent(Event.SERVER_CONNECTION_ERROR); return false; } Collection<TransactionOutputEx> remoteUnspent = UnspentOutputResponse.unspent; // Store the current block height setBlockChainHeight(UnspentOutputResponse.height); // Make a map for fast lookup Map<OutPoint, TransactionOutputEx> remoteMap = toMap(remoteUnspent); // Get the current unspent outputs as it is believed to be locally Collection<TransactionOutputEx> localUnspent = _backing.getAllUnspentOutputs(); // Make a map for fast lookup Map<OutPoint, TransactionOutputEx> localMap = toMap(localUnspent); // Find remotely removed unspent outputs for (TransactionOutputEx l : localUnspent) { TransactionOutputEx r = remoteMap.get(l.outPoint); if (r == null) { // An output has gone. Maybe it was spent in another wallet, or // never confirmed due to missing fees, double spend, or mutated. // Either way, we delete it locally _backing.deleteUnspentOutput(l.outPoint); } } // Find remotely added unspent outputs Set<Sha256Hash> transactionsToAddOrUpdate = new HashSet<Sha256Hash>(); List<TransactionOutputEx> unspentOutputsToAddOrUpdate = new LinkedList<TransactionOutputEx>(); for (TransactionOutputEx r : remoteUnspent) { TransactionOutputEx l = localMap.get(r.outPoint); if (l == null || l.height != r.height) { // New remote output or new height (Maybe it confirmed or we // might even have had a reorg). Either way we just update it unspentOutputsToAddOrUpdate.add(r); transactionsToAddOrUpdate.add(r.outPoint.hash); // Note: We are not adding the unspent output to the DB just yet. We // first want to verify the full set of funding transactions of the // transaction that this unspent output belongs to } } // Fetch updated or added transactions if (transactionsToAddOrUpdate.size() > 0) { GetTransactionsResponse response; try { response = _wapi .getTransactions( new GetTransactionsRequest(Wapi.VERSION, transactionsToAddOrUpdate)) .getResult(); } catch (WapiException e) { _logger.logError("Server connection failed with error code: " + e.errorCode, e); postEvent(Event.SERVER_CONNECTION_ERROR); return false; } try { handleNewExternalTransactions(response.transactions); } catch (WapiException e) { _logger.logError("Server connection failed with error code: " + e.errorCode, e); postEvent(Event.SERVER_CONNECTION_ERROR); return false; } // Finally update out list of unspent outputs with added or updated // outputs for (TransactionOutputEx output : unspentOutputsToAddOrUpdate) { _backing.putUnspentOutput(output); } } return true; }
@Override public TransactionEx getTransaction(Sha256Hash txid) { return _backing.getTransaction(txid); }