3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-12 18:25:51 +00:00

Re-organize the wallet sending APIs to take a SendRequest. Full details of the API changes are sent to the list.

This commit is contained in:
Mike Hearn 2012-09-06 14:22:05 +02:00
parent 8162aa0ed1
commit 29d5dcd424
3 changed files with 160 additions and 100 deletions

View File

@ -1241,50 +1241,130 @@ public class Wallet implements Serializable {
throw new RuntimeException("Unreachable");
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// SEND APIS
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** A SendResult is returned to you as part of sending coins to a recipient. */
public static class SendResult {
/** The Bitcoin transaction message that moves the money. */
public Transaction tx;
/** A future that will complete once the tx message has been successfully broadcast to the network. */
public ListenableFuture<Transaction> broadcastComplete;
}
/**
* Statelessly creates a transaction that sends the given number of nanocoins to address. The change is sent to
* {@link Wallet#getChangeAddress()}, so you must have added at least one key.<p>
* <p/>
* This method is stateless in the sense that calling it twice with the same inputs will result in two
* Transaction objects which are equal. The wallet is not updated to track its pending status or to mark the
* A SendRequest gives the wallet information about precisely how to send money to a recipient or set of recipients.
* Static methods are provided to help you create SendRequests and there are a few helper methods on the wallet that
* just simplify the most common use cases. You may wish to customize a SendRequest if you want to attach a fee or
* modify the change address.
*/
public static class SendRequest {
/**
* A transaction, probably incomplete, that describes the outline of what you want to do. This typically will
* mean it has some outputs to the intended destinations, but no inputs or change address (and therefore no
* fees) - the wallet will calculate all that for you and update tx later.
*/
public Transaction tx;
/**
* "Change" means the difference between the value gathered by a transactions inputs (the size of which you
* don't really control as it depends on who sent you money), and the value being sent somewhere else. The
* change address should be selected from this wallet, normally. <b>If null this will be chosen for you.</b>
*/
public Address changeAddress;
// Tracks if this has been passed to wallet.completeTx already: just a safety check.
private boolean completed;
private SendRequest() {}
public static SendRequest to(Address destination, BigInteger value) {
SendRequest req = new Wallet.SendRequest();
req.tx = new Transaction(destination.getParameters());
req.tx.addOutput(value, destination);
return req;
}
public static SendRequest to(NetworkParameters params, ECKey destination, BigInteger value) {
SendRequest req = new SendRequest();
req.tx = new Transaction(params);
req.tx.addOutput(value, destination);
return req;
}
/** Simply wraps a pre-built incomplete transaction provided by you. */
public static SendRequest forTx(Transaction tx) {
SendRequest req = new SendRequest();
req.tx = tx;
return req;
}
}
/*
* <p>Statelessly creates a transaction that sends the given value to address. The change is sent to
* {@link Wallet#getChangeAddress()}, so you must have added at least one key.</p>
*
* <p>If you just want to send money quickly, you probably want
* {@link Wallet#sendCoins(PeerGroup, Address, java.math.BigInteger)} instead. That will create the sending
* transaction, commit to the wallet and broadcast it to the network all in one go. This method is lower level
* and lets you see the proposed transaction before anything is done with it.</p>
*
* <p>This is a helper method that is equivalent to using {@link Wallet.SendRequest#to(Address, java.math.BigInteger)}
* followed by {@link Wallet#completeTx(com.google.bitcoin.core.Wallet.SendRequest)} and returning the requests
* transaction object. If you want more control over the process, just do those two steps yourself.</p>
*
* <p>IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions
* that spend the same coins. You have to call {@link Wallet#commitTx(Transaction)} on the created transaction to
* prevent this, but that should only occur once the transaction has been accepted by the network. This implies
* you cannot have more than one outstanding sending tx at once.</p>
*
* @param address The BitCoin address to send the money to.
* @param nanocoins How much currency to send, in nanocoins.
* @return either the created Transaction or null if there are insufficient coins.
* coins as spent until commitTx is called on the result.
*/
public synchronized Transaction createSend(Address address, BigInteger nanocoins) {
return createSend(address, nanocoins, getChangeAddress());
SendRequest req = SendRequest.to(address, nanocoins);
if (completeTx(req)) {
return req.tx;
} else {
return null; // No money.
}
}
/**
* Sends coins to the given address but does not broadcast the resulting pending transaction. It is still stored
* in the wallet, so when the wallet is added to a {@link PeerGroup} or {@link Peer} the transaction will be
* announced to the network.
* announced to the network. The given {@link SendRequest} is completed first using
* {@link Wallet#completeTx(com.google.bitcoin.core.Wallet.SendRequest)} to make it valid.
*
* @param to Address to send the coins to.
* @param nanocoins How many coins to send.
* @return the Transaction that was created, or null if there are insufficient coins in thew allet.
* @return the Transaction that was created, or null if there are insufficient coins in the wallet.
*/
public synchronized Transaction sendCoinsOffline(Address to, BigInteger nanocoins) {
Transaction tx = createSend(to, nanocoins);
if (tx == null) // Not enough money! :-(
return null;
public synchronized Transaction sendCoinsOffline(SendRequest request) {
try {
commitTx(tx);
if (!completeTx(request))
return null; // Not enough money! :-(
commitTx(request.tx);
return request.tx;
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves.
}
return tx;
}
public static class SendResult {
public Transaction tx;
public ListenableFuture<Transaction> broadcastComplete;
}
/**
* Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to
* {@link Wallet#getChangeAddress()}. The returned object provides both the transaction, and a future that can
* be used to learn when the broadcast is complete. Complete means, if the PeerGroup is limited to only one
* connection, when it was written out to the socket. Otherwise when the transaction is written out and we heard
* it back from a different peer.
* <p>Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to
* {@link Wallet#getChangeAddress()}. No fee is attached <b>even if one would be required</b>.</p>
*
* <p>The returned object provides both the transaction, and a future that can be used to learn when the broadcast
* is complete. Complete means, if the PeerGroup is limited to only one connection, when it was written out to
* the socket. Otherwise when the transaction is written out and we heard it back from a different peer.</p>
*
* <p>Note that the sending transaction is committed to the wallet immediately, not when the transaction is
* successfully broadcast. This means that even if the network hasn't heard about your transaction you won't be
* able to spend those same coins again.</p>
*
* @param peerGroup a PeerGroup to use for broadcast or null.
* @param to Which address to send coins to.
@ -1292,7 +1372,31 @@ public class Wallet implements Serializable {
* @return An object containing the transaction that was created, and a future for the broadcast of it.
*/
public SendResult sendCoins(PeerGroup peerGroup, Address to, BigInteger value) {
Transaction tx = sendCoinsOffline(to, value);
SendRequest request = SendRequest.to(to, value);
return sendCoins(peerGroup, request);
}
/**
* <p>Sends coins according to the given request, via the given {@link PeerGroup}.</p>
*
* <p>The returned object provides both the transaction, and a future that can be used to learn when the broadcast
* is complete. Complete means, if the PeerGroup is limited to only one connection, when it was written out to
* the socket. Otherwise when the transaction is written out and we heard it back from a different peer.</p>
*
* <p>Note that the sending transaction is committed to the wallet immediately, not when the transaction is
* successfully broadcast. This means that even if the network hasn't heard about your transaction you won't be
* able to spend those same coins again.</p>
*
* @param peerGroup a PeerGroup to use for broadcast or null.
* @param request the SendRequest that describes what to do, get one using static methods on SendRequest itself.
* @return An object containing the transaction that was created, and a future for the broadcast of it.
*/
public SendResult sendCoins(PeerGroup peerGroup, SendRequest request) {
// Does not need to be synchronized as sendCoinsOffline is and the rest is all thread-local.
// Commit the TX to the wallet immediately so the spent coins won't be reused.
// TODO: We should probably allow the request to specify tx commit only after the network has accepted it.
Transaction tx = sendCoinsOffline(request);
if (tx == null)
return null; // Not enough money.
SendResult result = new SendResult();
@ -1311,71 +1415,35 @@ public class Wallet implements Serializable {
* If an exception is thrown by {@link Peer#sendMessage(Message)} the transaction is still committed, so the
* pending transaction must be broadcast <b>by you</b> at some other time.
*
* @param to Which address to send coins to.
* @param nanocoins How many nanocoins to send. You can use Utils.toNanoCoins() to calculate this.
* @return The {@link Transaction} that was created or null if there was insufficient balance to send the coins.
* @throws IOException if there was a problem broadcasting the transaction
*/
public synchronized Transaction sendCoins(Peer peer, Address to, BigInteger nanocoins) throws IOException {
// TODO: This API is fairly questionable and the function isn't tested. If anything goes wrong during sending
// on the peer you don't get access to the created Transaction object and must fish it out of the wallet then
// do your own retry later.
Transaction tx = createSend(to, nanocoins);
if (tx == null) // Not enough money! :-(
return null;
try {
commitTx(tx);
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves.
}
public synchronized Transaction sendCoins(Peer peer, SendRequest request) throws IOException {
Transaction tx = sendCoinsOffline(request);
if (tx == null)
return null; // Not enough money.
peer.sendMessage(tx);
return tx;
}
/**
* Creates a transaction that sends $coins.$cents BTC to the given address.<p>
* <p/>
* IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions
* that spend the same coins. You have to call commitTx on the created transaction to prevent this,
* but that should only occur once the transaction has been accepted by the network. This implies you cannot have
* more than one outstanding sending tx at once.
* Given a spend request containing an incomplete transaction, makes it valid by adding inputs and outputs according
* to the instructions in the request. The transaction in the request is modified by this method.
*
* @param address The BitCoin address to send the money to.
* @param nanocoins How much currency to send, in nanocoins.
* @param changeAddress Which address to send the change to, in case we can't make exactly the right value from
* our coins. This should be an address we own (is in the keychain).
* @return a new {@link Transaction} or null if we cannot afford this send.
* @param req a SendRequest that contains the incomplete transaction and details for how to make it valid.
* @throws IllegalArgumentException if you try and complete the same SendRequest twice.
* @return False if we cannot afford this send, true otherwise.
*/
public synchronized Transaction createSend(Address address, BigInteger nanocoins, Address changeAddress) {
log.info("Creating send tx to " + address.toString() + " for " +
bitcoinValueToFriendlyString(nanocoins));
Transaction sendTx = new Transaction(params);
sendTx.addOutput(nanocoins, address);
if (completeTx(sendTx, changeAddress)) {
return sendTx;
} else {
return null;
}
}
/**
* Takes a transaction with arbitrary outputs, gathers the necessary inputs for spending, and signs it
* @param sendTx The transaction to complete
* @param changeAddress Which address to send the change to, in case we can't make exactly the right value from
* our coins. This should be an address we own (is in the keychain).
* @return False if we cannot afford this send, true otherwise
*/
public synchronized boolean completeTx(Transaction sendTx, Address changeAddress) {
public synchronized boolean completeTx(SendRequest req) {
Preconditions.checkArgument(!req.completed, "Given SendRequest has already been completed.");
// Calculate the transaction total
BigInteger nanocoins = BigInteger.ZERO;
for(TransactionOutput output : sendTx.getOutputs()) {
for (TransactionOutput output : req.tx.getOutputs()) {
nanocoins = nanocoins.add(output.getValue());
}
log.info("Completing send tx with {} outputs totalling {}", sendTx.getOutputs().size(), bitcoinValueToFriendlyString(nanocoins));
log.info("Completing send tx with {} outputs totalling {}",
req.tx.getOutputs().size(), bitcoinValueToFriendlyString(nanocoins));
// To send money to somebody else, we need to do gather up transactions with unspent outputs until we have
// sufficient value. Many coin selection algorithms are possible, we use a simple but suboptimal one.
@ -1403,41 +1471,33 @@ public class Wallet implements Serializable {
return false;
}
checkState(gathered.size() > 0);
sendTx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
req.tx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
BigInteger change = valueGathered.subtract(nanocoins);
if (change.compareTo(BigInteger.ZERO) > 0) {
// The value of the inputs is greater than what we want to send. Just like in real life then,
// we need to take back some coins ... this is called "change". Add another output that sends the change
// back to us.
log.info(" with " + bitcoinValueToFriendlyString(change) + " coins change");
sendTx.addOutput(new TransactionOutput(params, sendTx, change, changeAddress));
// back to us. The address comes either from the request or getChangeAddress() as a default.
Address changeAddress = req.changeAddress != null ? req.changeAddress : getChangeAddress();
log.info(" with {} coins change", bitcoinValueToFriendlyString(change));
req.tx.addOutput(new TransactionOutput(params, req.tx, change, changeAddress));
}
for (TransactionOutput output : gathered) {
sendTx.addInput(output);
req.tx.addInput(output);
}
// Now sign the inputs, thus proving that we are entitled to redeem the connected outputs.
try {
sendTx.signInputs(Transaction.SigHash.ALL, this);
req.tx.signInputs(Transaction.SigHash.ALL, this);
} catch (ScriptException e) {
// If this happens it means an output script in a wallet tx could not be understood. That should never
// happen, if it does it means the wallet has got into an inconsistent state.
throw new RuntimeException(e);
}
log.info(" completed {}", sendTx.getHashAsString());
req.completed = true;
log.info(" completed {}", req.tx.getHashAsString());
return true;
}
/**
* Takes a transaction with arbitrary outputs, gathers the necessary inputs for spending, and signs it.
* Change goes to {@link Wallet#getChangeAddress()}
* @param sendTx The transaction to complete
* @return False if we cannot afford this send, true otherwise
*/
public synchronized boolean completeTx(Transaction sendTx) {
return completeTx(sendTx, getChangeAddress());
}
synchronized Address getChangeAddress() {
// For now let's just pick the first key in our keychain. In future we might want to do something else to
// give the user better privacy here, eg in incognito mode.

View File

@ -338,7 +338,7 @@ public class PeerGroupTest extends TestWithNetworkConnections {
// Do the same thing with an offline transaction.
peerGroup.removeWallet(wallet);
Transaction t3 = wallet.sendCoinsOffline(dest, Utils.toNanoCoins(2, 0));
Transaction t3 = wallet.sendCoinsOffline(Wallet.SendRequest.to(dest, Utils.toNanoCoins(2, 0)));
assertNull(outbound(p1)); // Nothing sent.
// Add the wallet to the peer group (simulate initialization). Transactions should be announced.
peerGroup.addWallet(wallet);

View File

@ -116,7 +116,7 @@ public class WalletTest {
t2.addOutput(v2, a2);
t2.addOutput(v3, a2);
t2.addOutput(v4, a2);
boolean complete = wallet.completeTx(t2);
boolean complete = wallet.completeTx(Wallet.SendRequest.forTx(t2));
// Do some basic sanity checks.
assertTrue(complete);
@ -230,7 +230,7 @@ public class WalletTest {
assertEquals(TransactionConfidence.ConfidenceType.BUILDING, tx1.getConfidence().getConfidenceType());
assertEquals(1, tx1.getConfidence().getAppearedAtChainHeight());
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10));
// Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we
// want to get back to our previous state. We can do this by just not confirming the transaction as
// createSend is stateless.
@ -243,7 +243,7 @@ public class WalletTest {
assertEquals(bitcoinValueToFriendlyString(bigints[2]), "1.00");
assertEquals(bitcoinValueToFriendlyString(bigints[3]), "0.90");
// And we do it again after the catchup.
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10));
// What we'd really like to do is prove the official client would accept it .... no such luck unfortunately.
wallet.commitTx(send2);
StoredBlock b3 = createFakeBlock(params, blockStore, send2).storedBlock;
@ -258,7 +258,7 @@ public class WalletTest {
wallet.receiveFromBlock(tx1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(nanos, tx1.getValueSentToMe(wallet, true));
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10));
// Reserialize.
Transaction send2 = new Transaction(params, send1.bitcoinSerialize());
assertEquals(nanos, send2.getValueSentFromMe(wallet));
@ -809,7 +809,7 @@ public class WalletTest {
Transaction t2 = new Transaction(params);
TransactionOutput o2 = new TransactionOutput(params, t2, v2, k2.toAddress(params));
t2.addOutput(o2);
boolean complete = wallet.completeTx(t2);
boolean complete = wallet.completeTx(Wallet.SendRequest.forTx(t2));
assertTrue(complete);
// Commit t2, so it is placed in the pending pool