mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-07-31 20:11:23 +00:00
Issue 586 fixed, 0BTC transaction with OP_RETURN will work.
This commit is contained in:
committed by
Andreas Schildbach
parent
855fd2832f
commit
dd37fe90c6
@@ -253,7 +253,7 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
||||
|
||||
/**
|
||||
* Returns the minimum value for this output to be considered "not dust", i.e. the transaction will be relayable
|
||||
* and mined by default miners. For normal pay to address outputs, this is 5460 satoshis, the same as
|
||||
* and mined by default miners. For normal pay to address outputs, this is 546 satoshis, the same as
|
||||
* {@link Transaction#MIN_NONDUST_OUTPUT}.
|
||||
*/
|
||||
public Coin getMinNonDustValue() {
|
||||
|
@@ -57,8 +57,6 @@ import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
@@ -3371,6 +3369,8 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
|
||||
public static class CompletionException extends RuntimeException {}
|
||||
public static class DustySendRequested extends CompletionException {}
|
||||
public static class MultipleOpReturnRequested extends CompletionException {}
|
||||
|
||||
/**
|
||||
* Thrown when we were trying to empty the wallet, and the total amount of money we were trying to empty after
|
||||
* being reduced for the fee was smaller than the min payment. Note that the missing field will be null in this
|
||||
@@ -3413,17 +3413,29 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
value = value.subtract(totalInput);
|
||||
|
||||
List<TransactionInput> originalInputs = new ArrayList<TransactionInput>(req.tx.getInputs());
|
||||
int opReturnCount = 0;
|
||||
|
||||
// We need to know if we need to add an additional fee because one of our values are smaller than 0.01 BTC
|
||||
boolean needAtLeastReferenceFee = false;
|
||||
if (req.ensureMinRequiredFee && !req.emptyWallet) { // min fee checking is handled later for emptyWallet
|
||||
for (TransactionOutput output : req.tx.getOutputs())
|
||||
if (req.ensureMinRequiredFee && !req.emptyWallet) { // Min fee checking is handled later for emptyWallet.
|
||||
for (TransactionOutput output : req.tx.getOutputs()) {
|
||||
if (output.getValue().compareTo(Coin.CENT) < 0) {
|
||||
if (output.getValue().compareTo(output.getMinNonDustValue()) < 0)
|
||||
throw new DustySendRequested();
|
||||
needAtLeastReferenceFee = true;
|
||||
if (output.getValue().compareTo(output.getMinNonDustValue()) < 0) { // Is transaction a "dust".
|
||||
if (output.getScriptPubKey().isOpReturn()) { // Transactions that are OP_RETURN can't be dust regardless of their value.
|
||||
++opReturnCount;
|
||||
continue;
|
||||
} else {
|
||||
throw new DustySendRequested();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opReturnCount > 1) { // Only 1 OP_RETURN per transaction allowed.
|
||||
throw new MultipleOpReturnRequested();
|
||||
}
|
||||
|
||||
// Calculate a list of ALL potential candidates for spending and then ask a coin selector to provide us
|
||||
|
@@ -725,6 +725,10 @@ public class Script {
|
||||
return Utils.decodeMPI(Utils.reverseBytes(chunk), false);
|
||||
}
|
||||
|
||||
public boolean isOpReturn() {
|
||||
return chunks.size() == 2 && chunks.get(0).equalsOpCode(OP_RETURN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the script interpreter. Normally you should not use this directly, instead use
|
||||
* {@link org.bitcoinj.core.TransactionInput#verify(org.bitcoinj.core.TransactionOutput)} or
|
||||
|
@@ -19,6 +19,9 @@ package org.bitcoinj.core;
|
||||
|
||||
import org.bitcoinj.core.Wallet.SendRequest;
|
||||
import org.bitcoinj.crypto.*;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.bitcoinj.signers.StatelessTransactionSigner;
|
||||
import org.bitcoinj.signers.TransactionSigner;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
@@ -362,28 +365,31 @@ public class WalletTest extends TestWithWallet {
|
||||
}
|
||||
|
||||
private void receiveATransaction(Wallet wallet, Address toAddress) throws Exception {
|
||||
Coin v1 = COIN;
|
||||
final ListenableFuture<Coin> availFuture = wallet.getBalanceFuture(v1, Wallet.BalanceType.AVAILABLE);
|
||||
final ListenableFuture<Coin> estimatedFuture = wallet.getBalanceFuture(v1, Wallet.BalanceType.ESTIMATED);
|
||||
receiveATransactionAmount(wallet, toAddress, COIN);
|
||||
}
|
||||
|
||||
private void receiveATransactionAmount(Wallet wallet, Address toAddress, Coin amount) throws IOException {
|
||||
final ListenableFuture<Coin> availFuture = wallet.getBalanceFuture(amount, Wallet.BalanceType.AVAILABLE);
|
||||
final ListenableFuture<Coin> estimatedFuture = wallet.getBalanceFuture(amount, Wallet.BalanceType.ESTIMATED);
|
||||
assertFalse(availFuture.isDone());
|
||||
assertFalse(estimatedFuture.isDone());
|
||||
// Send some pending coins to the wallet.
|
||||
Transaction t1 = sendMoneyToWallet(wallet, v1, toAddress, null);
|
||||
Transaction t1 = sendMoneyToWallet(wallet, amount, toAddress, null);
|
||||
Threading.waitForUserCode();
|
||||
final ListenableFuture<Transaction> depthFuture = t1.getConfidence().getDepthFuture(1);
|
||||
assertFalse(depthFuture.isDone());
|
||||
assertEquals(ZERO, wallet.getBalance());
|
||||
assertEquals(v1, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
assertEquals(amount, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
assertFalse(availFuture.isDone());
|
||||
// Our estimated balance has reached the requested level.
|
||||
assertTrue(estimatedFuture.isDone());
|
||||
assertEquals(1, wallet.getPoolSize(Pool.PENDING));
|
||||
assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
|
||||
assertEquals(0, wallet.getPoolSize(Pool.UNSPENT));
|
||||
// Confirm the coins.
|
||||
sendMoneyToWallet(wallet, t1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals("Incorrect confirmed tx balance", v1, wallet.getBalance());
|
||||
assertEquals("Incorrect confirmed tx PENDING pool size", 0, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
|
||||
assertEquals("Incorrect confirmed tx UNSPENT pool size", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
|
||||
assertEquals("Incorrect confirmed tx balance", amount, wallet.getBalance());
|
||||
assertEquals("Incorrect confirmed tx PENDING pool size", 0, wallet.getPoolSize(Pool.PENDING));
|
||||
assertEquals("Incorrect confirmed tx UNSPENT pool size", 1, wallet.getPoolSize(Pool.UNSPENT));
|
||||
assertEquals("Incorrect confirmed tx ALL pool size", 1, wallet.getTransactions(true).size());
|
||||
Threading.waitForUserCode();
|
||||
assertTrue(availFuture.isDone());
|
||||
@@ -1596,6 +1602,107 @@ public class WalletTest extends TestWithWallet {
|
||||
wallet.completeTx(req);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void opReturnOneOutputTest() throws Exception {
|
||||
// Tests basic send of transaction with one output that doesn't transfer any value but just writes OP_RETURN.
|
||||
receiveATransaction(wallet, myAddress);
|
||||
Transaction tx = new Transaction(params);
|
||||
Coin messagePrice = Coin.ZERO;
|
||||
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
|
||||
tx.addOutput(messagePrice, script);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void opReturnOneOutputWithValueTest() throws Exception {
|
||||
// Tests basic send of transaction with one output that destroys coins and has an OP_RETURN.
|
||||
receiveATransaction(wallet, myAddress);
|
||||
Transaction tx = new Transaction(params);
|
||||
Coin messagePrice = CENT;
|
||||
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
|
||||
tx.addOutput(messagePrice, script);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void opReturnTwoOutputsTest() throws Exception {
|
||||
// Tests sending transaction where one output transfers BTC, the other one writes OP_RETURN.
|
||||
receiveATransaction(wallet, myAddress);
|
||||
Address notMyAddr = new ECKey().toAddress(params);
|
||||
Transaction tx = new Transaction(params);
|
||||
Coin messagePrice = Coin.ZERO;
|
||||
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
|
||||
tx.addOutput(CENT, notMyAddr);
|
||||
tx.addOutput(messagePrice, script);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test(expected = Wallet.MultipleOpReturnRequested.class)
|
||||
public void twoOpReturnsPerTransactionTest() throws Exception {
|
||||
// Tests sending transaction where there are 2 attempts to write OP_RETURN scripts - this should fail and throw MultipleOpReturnRequested.
|
||||
receiveATransaction(wallet, myAddress);
|
||||
Transaction tx = new Transaction(params);
|
||||
Coin messagePrice = Coin.ZERO;
|
||||
Script script1 = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world 1!".getBytes()).build();
|
||||
Script script2 = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world 2!".getBytes()).build();
|
||||
tx.addOutput(messagePrice, script1);
|
||||
tx.addOutput(messagePrice, script2);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test(expected = Wallet.DustySendRequested.class)
|
||||
public void sendDustTest() throws InsufficientMoneyException {
|
||||
// Tests sending dust, should throw DustySendRequested.
|
||||
Transaction tx = new Transaction(params);
|
||||
Address notMyAddr = new ECKey().toAddress(params);
|
||||
tx.addOutput(Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI), notMyAddr);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendMultipleCentsTest() throws Exception {
|
||||
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
|
||||
Transaction tx = new Transaction(params);
|
||||
Address notMyAddr = new ECKey().toAddress(params);
|
||||
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
|
||||
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
|
||||
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
|
||||
tx.addOutput(COIN.CENT.subtract(SATOSHI), notMyAddr);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test(expected = Wallet.DustySendRequested.class)
|
||||
public void sendDustAndOpReturnWithoutValueTest() throws Exception {
|
||||
// Tests sending dust and OP_RETURN without value, should throw DustySendRequested because sending sending dust is not allowed in any case.
|
||||
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
|
||||
Transaction tx = new Transaction(params);
|
||||
Address notMyAddr = new ECKey().toAddress(params);
|
||||
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
|
||||
tx.addOutput(Coin.ZERO, script);
|
||||
tx.addOutput(Coin.SATOSHI, notMyAddr);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test(expected = Wallet.DustySendRequested.class)
|
||||
public void sendDustAndMessageWithValueTest() throws Exception {
|
||||
//Tests sending dust and OP_RETURN with value, should throw DustySendRequested
|
||||
receiveATransaction(wallet, myAddress);
|
||||
Transaction tx = new Transaction(params);
|
||||
Address notMyAddr = new ECKey().toAddress(params);
|
||||
Script script = new ScriptBuilder().op(ScriptOpCodes.OP_RETURN).data("hello world!".getBytes()).build();
|
||||
tx.addOutput(Coin.CENT, script);
|
||||
tx.addOutput(Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI), notMyAddr);
|
||||
SendRequest request = Wallet.SendRequest.forTx(tx);
|
||||
wallet.completeTx(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void feeSolverAndCoinSelectionTest() throws Exception {
|
||||
// Tests basic fee solving works
|
||||
|
Reference in New Issue
Block a user