3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-01 07:42:17 +00:00

Split transaction creation into building a template and then completing it. Completing a tx means adding inputs and possibly a change output to make the transaction valid. Also add a convenience addOutput() overload to Transaction. This makes it easier to create multi-sends.

Patch from Chris Rico.
This commit is contained in:
Mike Hearn 2012-02-07 21:47:21 +01:00
parent b43b686264
commit 11a87317a4
4 changed files with 101 additions and 17 deletions

View File

@ -11,3 +11,4 @@ Wolfgang Nagele <wolfgang.nagele@gmail.com>
Jonny Heggheim <hegjon@gmail.com>
Steve Coughlan <shadders.del@gmail.com>
Roman Mandeleil <roman.mandeleil@gmail.com>
Chris Rico <chrisrico@gmail.com>

View File

@ -558,6 +558,13 @@ public class Transaction extends ChildMessage implements Serializable {
adjustLength(to.length);
}
/**
* Creates an output based on the given address and value, adds it to this transaction.
*/
public void addOutput(BigInteger value, Address address) {
addOutput(new TransactionOutput(params, this, value, address));
}
/**
* Once a transaction has some inputs and outputs added, the signatures in the inputs can be calculated. The
* signature is over the transaction itself, to prove the redeemer actually created that transaction,

View File

@ -679,7 +679,7 @@ public class Wallet implements Serializable {
* and we did not create it, and it spends some of our outputs.</li>
* </ol>
*/
synchronized void commitTx(Transaction tx) throws VerificationException {
public synchronized void commitTx(Transaction tx) throws VerificationException {
assert !pending.containsKey(tx.getHash()) : "commitTx called on the same transaction twice";
log.info("commitTx of {}", tx.getHashAsString());
tx.updatedAt = Utils.now();
@ -884,18 +884,14 @@ public class Wallet implements Serializable {
/**
* Statelessly creates a transaction that sends the given number of nanocoins to address. The change is sent to
* the first address in the wallet, so you must have added at least one key.<p>
* {@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
* coins as spent until commitTx is called on the result.
*/
synchronized Transaction createSend(Address address, BigInteger nanocoins) {
// 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.
assert keychain.size() > 0 : "Can't send value without an address to use for receiving change";
ECKey first = keychain.get(0);
return createSend(address, nanocoins, first.toAddress(params));
return createSend(address, nanocoins, getChangeAddress());
}
/**
@ -920,8 +916,8 @@ public class Wallet implements Serializable {
}
/**
* Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to the first key in the
* wallet. The transaction will be announced to any connected nodes asynchronously. If you would like to know when
* Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to {@link Wallet#getChangeAddress()}.
* The transaction will be announced to any connected nodes asynchronously. If you would like to know when
* the transaction was successfully sent to at least one node, use
* {@link Wallet#sendCoinsOffline(Address, java.math.BigInteger)} and then {@link PeerGroup#broadcastTransaction(Transaction)}
* on the result to obtain a {@link java.util.concurrent.Future<Transaction>}.
@ -941,8 +937,8 @@ public class Wallet implements Serializable {
}
/**
* Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to the first key in the
* wallet. The method will block until the transaction has been announced to at least one node.
* Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to {@link Wallet#getChangeAddress()}.
* The method will block until the transaction has been announced to at least one node.
*
* @param peerGroup a PeerGroup to use for broadcast or null.
* @param to Which address to send coins to.
@ -961,7 +957,7 @@ public class Wallet implements Serializable {
}
/**
* Sends coins to the given address, via the given {@link Peer}. Change is returned to the first key in the wallet.
* Sends coins to the given address, via the given {@link Peer}. Change is returned to {@link Wallet#getChangeAddress()}.
* 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.
*
@ -1004,6 +1000,33 @@ public class Wallet implements Serializable {
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) {
// Calculate the transaction total
BigInteger nanocoins = BigInteger.ZERO;
for(TransactionOutput output : sendTx.getOutputs()) {
nanocoins = nanocoins.add(output.getValue());
}
log.info("Completing send tx with {} outputs totalling {}", sendTx.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.
// TODO: Sort coins so we use the smallest first, to combat wallet fragmentation and reduce fees.
@ -1023,12 +1046,10 @@ public class Wallet implements Serializable {
log.info("Insufficient value in wallet for send, missing " +
bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered)));
// TODO: Should throw an exception here.
return null;
return false;
}
assert gathered.size() > 0;
Transaction sendTx = new Transaction(params);
sendTx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
sendTx.addOutput(new TransactionOutput(params, sendTx, nanocoins, address));
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,
@ -1049,8 +1070,26 @@ public class Wallet implements Serializable {
// happen, if it does it means the wallet has got into an inconsistent state.
throw new RuntimeException(e);
}
log.info(" created {}", sendTx.getHashAsString());
return sendTx;
log.info(" completed {}", sendTx.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.
assert keychain.size() > 0 : "Can't send value without an address to use for receiving change";
ECKey first = keychain.get(0);
return first.toAddress(params);
}
/**

View File

@ -85,6 +85,43 @@ public class WalletTest {
assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL));
}
@Test
public void customTransactionSpending() throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
BigInteger v1 = Utils.toNanoCoins(3, 0);
Transaction t1 = createFakeTx(params, v1, myAddress);
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(v1, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
ECKey k2 = new ECKey();
Address a2 = k2.toAddress(params);
BigInteger v2 = toNanoCoins(0, 50);
BigInteger v3 = toNanoCoins(0, 75);
BigInteger v4 = toNanoCoins(1, 25);
Transaction t2 = new Transaction(params);
t2.addOutput(v2, a2);
t2.addOutput(v3, a2);
t2.addOutput(v4, a2);
boolean complete = wallet.completeTx(t2);
// Do some basic sanity checks.
assertTrue(complete);
assertEquals(1, t2.getInputs().size());
assertEquals(myAddress, t2.getInputs().get(0).getScriptSig().getFromAddress());
assertEquals(t2.getConfidence().getConfidenceType(), TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
// We have NOT proven that the signature is correct!
wallet.commitTx(t2);
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.SPENT));
assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL));
}
@Test
public void sideChain() throws Exception {
// The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance.