mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-12 18:25:51 +00:00
Support CLTV micropayment channels
Also extend WalletTool to send via, settle and refund these channels.
This commit is contained in:
parent
25db735b3a
commit
c9cce47962
@ -33,7 +33,6 @@ import org.bitcoinj.core.listeners.WalletChangeEventListener;
|
||||
import org.bitcoinj.core.listeners.WalletCoinEventListener;
|
||||
import org.bitcoinj.core.TransactionConfidence.*;
|
||||
import org.bitcoinj.crypto.*;
|
||||
import org.bitcoinj.params.*;
|
||||
import org.bitcoinj.script.*;
|
||||
import org.bitcoinj.signers.*;
|
||||
import org.bitcoinj.store.*;
|
||||
@ -3643,6 +3642,24 @@ public class Wallet extends BaseTaggableObject
|
||||
return req;
|
||||
}
|
||||
|
||||
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, Date releaseTime, ECKey from, ECKey to, Coin value) {
|
||||
long time = releaseTime.getTime() / 1000L;
|
||||
checkArgument(time >= Transaction.LOCKTIME_THRESHOLD, "Release time was too small");
|
||||
return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value);
|
||||
}
|
||||
|
||||
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, long lockTime, ECKey from, ECKey to, Coin value) {
|
||||
return toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), from, to, value);
|
||||
}
|
||||
|
||||
private static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) {
|
||||
SendRequest req = new SendRequest();
|
||||
Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
|
||||
req.tx = new Transaction(params);
|
||||
req.tx.addOutput(value, output);
|
||||
return req;
|
||||
}
|
||||
|
||||
/** Copy data from payment request. */
|
||||
public SendRequest fromPaymentDetails(PaymentDetails paymentDetails) {
|
||||
if (paymentDetails.hasMemo())
|
||||
@ -4120,6 +4137,19 @@ public class Wallet extends BaseTaggableObject
|
||||
if (key != null && (key.isEncrypted() || key.hasPrivKey()))
|
||||
return true;
|
||||
}
|
||||
} else if (script.isSentToCLTVPaymentChannel()) {
|
||||
// Any script for which we are the recipient or sender counts.
|
||||
byte[] sender = script.getCLTVPaymentChannelSenderPubKey();
|
||||
ECKey senderKey = findKeyFromPubKey(sender);
|
||||
if (senderKey != null && (senderKey.isEncrypted() || senderKey.hasPrivKey())) {
|
||||
return true;
|
||||
}
|
||||
byte[] recipient = script.getCLTVPaymentChannelRecipientPubKey();
|
||||
ECKey recipientKey = findKeyFromPubKey(sender);
|
||||
if (recipientKey != null && (recipientKey.isEncrypted() || recipientKey.hasPrivKey())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -302,6 +302,37 @@ public class Script {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sender public key from a LOCKTIMEVERIFY transaction
|
||||
* @return
|
||||
* @throws ScriptException
|
||||
*/
|
||||
public byte[] getCLTVPaymentChannelSenderPubKey() throws ScriptException {
|
||||
if (!isSentToCLTVPaymentChannel()) {
|
||||
throw new ScriptException("Script not a standard CHECKLOCKTIMVERIFY transaction: " + this);
|
||||
}
|
||||
return chunks.get(8).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recipient public key from a LOCKTIMEVERIFY transaction
|
||||
* @return
|
||||
* @throws ScriptException
|
||||
*/
|
||||
public byte[] getCLTVPaymentChannelRecipientPubKey() throws ScriptException {
|
||||
if (!isSentToCLTVPaymentChannel()) {
|
||||
throw new ScriptException("Script not a standard CHECKLOCKTIMVERIFY transaction: " + this);
|
||||
}
|
||||
return chunks.get(1).data;
|
||||
}
|
||||
|
||||
public BigInteger getCLTVPaymentChannelExpiry() {
|
||||
if (!isSentToCLTVPaymentChannel()) {
|
||||
throw new ScriptException("Script not a standard CHECKLOCKTIMEVERIFY transaction: " + this);
|
||||
}
|
||||
return castToBigInteger(chunks.get(4).data, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* For 2-element [input] scripts assumes that the paid-to-address can be derived from the public key.
|
||||
* The concept of a "from address" isn't well defined in Bitcoin and you should not assume the sender of a
|
||||
@ -686,6 +717,22 @@ public class Script {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isSentToCLTVPaymentChannel() {
|
||||
if (chunks.size() != 10) return false;
|
||||
// Check that opcodes match the pre-determined format.
|
||||
if (!chunks.get(0).equalsOpCode(OP_IF)) return false;
|
||||
// chunk[1] = recipient pubkey
|
||||
if (!chunks.get(2).equalsOpCode(OP_CHECKSIGVERIFY)) return false;
|
||||
if (!chunks.get(3).equalsOpCode(OP_ELSE)) return false;
|
||||
// chunk[4] = locktime
|
||||
if (!chunks.get(5).equalsOpCode(OP_CHECKLOCKTIMEVERIFY)) return false;
|
||||
if (!chunks.get(6).equalsOpCode(OP_DROP)) return false;
|
||||
if (!chunks.get(7).equalsOpCode(OP_ENDIF)) return false;
|
||||
// chunk[8] = sender pubkey
|
||||
if (!chunks.get(9).equalsOpCode(OP_CHECKSIG)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean equalsRange(byte[] a, int start, byte[] b) {
|
||||
if (start + b.length > a.length)
|
||||
return false;
|
||||
|
@ -23,6 +23,7 @@ import org.bitcoinj.core.Utils;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -435,4 +436,32 @@ public class ScriptBuilder {
|
||||
checkArgument(data.length <= 40);
|
||||
return new ScriptBuilder().op(OP_RETURN).data(data).build();
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelOutput(BigInteger time, ECKey from, ECKey to) {
|
||||
byte[] timeBytes = Utils.reverseBytes(Utils.encodeMPI(time, false));
|
||||
if (timeBytes.length > 5) {
|
||||
throw new RuntimeException("Time too large to encode as 5-byte int");
|
||||
}
|
||||
return new ScriptBuilder().op(OP_IF)
|
||||
.data(to.getPubKey()).op(OP_CHECKSIGVERIFY)
|
||||
.op(OP_ELSE)
|
||||
.data(timeBytes).op(OP_CHECKLOCKTIMEVERIFY).op(OP_DROP)
|
||||
.op(OP_ENDIF)
|
||||
.data(from.getPubKey()).op(OP_CHECKSIG).build();
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelRefund(TransactionSignature signature) {
|
||||
ScriptBuilder builder = new ScriptBuilder();
|
||||
builder.data(signature.encodeToBitcoin());
|
||||
builder.data(new byte[] { 0 }); // Use the CHECKLOCKTIMEVERIFY if branch
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) {
|
||||
ScriptBuilder builder = new ScriptBuilder();
|
||||
builder.data(from.encodeToBitcoin());
|
||||
builder.data(to.encodeToBitcoin());
|
||||
builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
package org.bitcoinj.core;
|
||||
|
||||
import org.bitcoinj.core.TransactionConfidence.*;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.params.*;
|
||||
import org.bitcoinj.script.*;
|
||||
import org.bitcoinj.testing.*;
|
||||
import org.easymock.*;
|
||||
import org.junit.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import static org.bitcoinj.core.BlockTest.params;
|
||||
import static org.bitcoinj.core.Utils.HEX;
|
||||
@ -205,6 +207,86 @@ public class TransactionTest {
|
||||
assertEquals(tx.isMature(), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCLTVPaymentChannelTransactionSpending() {
|
||||
BigInteger time = BigInteger.valueOf(20);
|
||||
|
||||
ECKey from = new ECKey(), to = new ECKey(), incorrect = new ECKey();
|
||||
Script outputScript = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
|
||||
|
||||
Transaction tx = new Transaction(PARAMS);
|
||||
tx.addInput(new TransactionInput(PARAMS, tx, new byte[] {}));
|
||||
tx.getInput(0).setSequenceNumber(0);
|
||||
tx.setLockTime(time.subtract(BigInteger.ONE).longValue());
|
||||
TransactionSignature fromSig =
|
||||
tx.calculateSignature(0, from, outputScript, Transaction.SigHash.SINGLE, false);
|
||||
TransactionSignature toSig =
|
||||
tx.calculateSignature(0, to, outputScript, Transaction.SigHash.SINGLE, false);
|
||||
TransactionSignature incorrectSig =
|
||||
tx.calculateSignature(0, incorrect, outputScript, Transaction.SigHash.SINGLE, false);
|
||||
Script scriptSig =
|
||||
ScriptBuilder.createCLTVPaymentChannelInput(fromSig, toSig);
|
||||
Script refundSig =
|
||||
ScriptBuilder.createCLTVPaymentChannelRefund(fromSig);
|
||||
Script invalidScriptSig1 =
|
||||
ScriptBuilder.createCLTVPaymentChannelInput(fromSig, incorrectSig);
|
||||
Script invalidScriptSig2 =
|
||||
ScriptBuilder.createCLTVPaymentChannelInput(incorrectSig, toSig);
|
||||
|
||||
try {
|
||||
scriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
|
||||
} catch (ScriptException e) {
|
||||
e.printStackTrace();
|
||||
fail("Settle transaction failed to correctly spend the payment channel");
|
||||
}
|
||||
|
||||
try {
|
||||
refundSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
|
||||
fail("Refund passed before expiry");
|
||||
} catch (ScriptException e) { }
|
||||
try {
|
||||
invalidScriptSig1.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
|
||||
fail("Invalid sig 1 passed");
|
||||
} catch (ScriptException e) { }
|
||||
try {
|
||||
invalidScriptSig2.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
|
||||
fail("Invalid sig 2 passed");
|
||||
} catch (ScriptException e) { }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCLTVPaymentChannelTransactionRefund() {
|
||||
BigInteger time = BigInteger.valueOf(20);
|
||||
|
||||
ECKey from = new ECKey(), to = new ECKey(), incorrect = new ECKey();
|
||||
Script outputScript = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
|
||||
|
||||
Transaction tx = new Transaction(PARAMS);
|
||||
tx.addInput(new TransactionInput(PARAMS, tx, new byte[] {}));
|
||||
tx.getInput(0).setSequenceNumber(0);
|
||||
tx.setLockTime(time.add(BigInteger.ONE).longValue());
|
||||
TransactionSignature fromSig =
|
||||
tx.calculateSignature(0, from, outputScript, Transaction.SigHash.SINGLE, false);
|
||||
TransactionSignature incorrectSig =
|
||||
tx.calculateSignature(0, incorrect, outputScript, Transaction.SigHash.SINGLE, false);
|
||||
Script scriptSig =
|
||||
ScriptBuilder.createCLTVPaymentChannelRefund(fromSig);
|
||||
Script invalidScriptSig =
|
||||
ScriptBuilder.createCLTVPaymentChannelRefund(incorrectSig);
|
||||
|
||||
try {
|
||||
scriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
|
||||
} catch (ScriptException e) {
|
||||
e.printStackTrace();
|
||||
fail("Refund failed to correctly spend the payment channel");
|
||||
}
|
||||
|
||||
try {
|
||||
invalidScriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS);
|
||||
fail("Invalid sig passed");
|
||||
} catch (ScriptException e) { }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() {
|
||||
Transaction tx = newTransaction();
|
||||
|
@ -406,6 +406,12 @@ public class ScriptTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCLTVPaymentChannelOutput() {
|
||||
Script script = ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(20), new ECKey(), new ECKey());
|
||||
assertTrue("script is locktime-verify", script.isSentToCLTVPaymentChannel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getToAddress() throws Exception {
|
||||
// pay to pubkey
|
||||
|
@ -19,10 +19,7 @@ package org.bitcoinj.tools;
|
||||
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.core.Wallet.BalanceType;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.crypto.KeyCrypterException;
|
||||
import org.bitcoinj.crypto.MnemonicCode;
|
||||
import org.bitcoinj.crypto.MnemonicException;
|
||||
import org.bitcoinj.crypto.*;
|
||||
import org.bitcoinj.net.discovery.DnsDiscovery;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
@ -30,6 +27,7 @@ import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.protocols.payments.PaymentProtocol;
|
||||
import org.bitcoinj.protocols.payments.PaymentProtocolException;
|
||||
import org.bitcoinj.protocols.payments.PaymentSession;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.store.*;
|
||||
import org.bitcoinj.uri.BitcoinURI;
|
||||
import org.bitcoinj.uri.BitcoinURIParseException;
|
||||
@ -173,6 +171,9 @@ public class WalletTool {
|
||||
SYNC,
|
||||
RESET,
|
||||
SEND,
|
||||
SEND_CLTVPAYMENTCHANNEL,
|
||||
SETTLE_CLTVPAYMENTCHANNEL,
|
||||
REFUND_CLTVPAYMENTCHANNEL,
|
||||
ENCRYPT,
|
||||
DECRYPT,
|
||||
MARRY,
|
||||
@ -228,6 +229,8 @@ public class WalletTool {
|
||||
parser.accepts("no-pki");
|
||||
parser.accepts("tor");
|
||||
parser.accepts("dump-privkeys");
|
||||
OptionSpec<String> refundFlag = parser.accepts("refund-to").withRequiredArg();
|
||||
OptionSpec<String> txHashFlag = parser.accepts("txhash").withRequiredArg();
|
||||
options = parser.parse(args);
|
||||
|
||||
if (args.length == 0 || options.has("help") ||
|
||||
@ -366,6 +369,59 @@ public class WalletTool {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SEND_CLTVPAYMENTCHANNEL: {
|
||||
if (!options.has(outputFlag)) {
|
||||
System.err.println("You must specify a --output=addr:value");
|
||||
return;
|
||||
}
|
||||
Coin fee = Coin.ZERO;
|
||||
if (options.has("fee")) {
|
||||
fee = parseCoin((String) options.valueOf("fee"));
|
||||
}
|
||||
if (!options.has("locktime")) {
|
||||
System.err.println("You must specify a --locktime");
|
||||
return;
|
||||
}
|
||||
String lockTime = (String) options.valueOf("locktime");
|
||||
boolean allowUnconfirmed = options.has("allow-unconfirmed");
|
||||
if (!options.has(refundFlag)) {
|
||||
System.err.println("You must specify an address to refund money to after expiry: --refund-to=addr");
|
||||
return;
|
||||
}
|
||||
sendCLTVPaymentChannel(refundFlag.value(options), outputFlag.value(options), fee, lockTime, allowUnconfirmed);
|
||||
} break;
|
||||
case SETTLE_CLTVPAYMENTCHANNEL: {
|
||||
if (!options.has(outputFlag)) {
|
||||
System.err.println("You must specify a --output=addr:value");
|
||||
return;
|
||||
}
|
||||
Coin fee = Coin.ZERO;
|
||||
if (options.has("fee")) {
|
||||
fee = parseCoin((String) options.valueOf("fee"));
|
||||
}
|
||||
boolean allowUnconfirmed = options.has("allow-unconfirmed");
|
||||
if (!options.has(txHashFlag)) {
|
||||
System.err.println("You must specify the transaction to spend: --txhash=tx-hash");
|
||||
return;
|
||||
}
|
||||
settleCLTVPaymentChannel(txHashFlag.value(options), outputFlag.value(options), fee, allowUnconfirmed);
|
||||
} break;
|
||||
case REFUND_CLTVPAYMENTCHANNEL: {
|
||||
if (!options.has(outputFlag)) {
|
||||
System.err.println("You must specify a --output=addr:value");
|
||||
return;
|
||||
}
|
||||
Coin fee = Coin.ZERO;
|
||||
if (options.has("fee")) {
|
||||
fee = parseCoin((String) options.valueOf("fee"));
|
||||
}
|
||||
boolean allowUnconfirmed = options.has("allow-unconfirmed");
|
||||
if (!options.has(txHashFlag)) {
|
||||
System.err.println("You must specify the transaction to spend: --txhash=tx-hash");
|
||||
return;
|
||||
}
|
||||
refundCLTVPaymentChannel(txHashFlag.value(options), outputFlag.value(options), fee, allowUnconfirmed);
|
||||
} break;
|
||||
case ENCRYPT: encrypt(); break;
|
||||
case DECRYPT: decrypt(); break;
|
||||
case MARRY: marry(); break;
|
||||
@ -513,36 +569,25 @@ public class WalletTool {
|
||||
// Convert the input strings to outputs.
|
||||
Transaction t = new Transaction(params);
|
||||
for (String spec : outputs) {
|
||||
String[] parts = spec.split(":");
|
||||
if (parts.length != 2) {
|
||||
System.err.println("Malformed output specification, must have two parts separated by :");
|
||||
return;
|
||||
}
|
||||
String destination = parts[0];
|
||||
try {
|
||||
Coin value;
|
||||
if ("ALL".equalsIgnoreCase(parts[1]))
|
||||
value = wallet.getBalance(BalanceType.ESTIMATED);
|
||||
else
|
||||
value = parseCoin(parts[1]);
|
||||
if (destination.startsWith("0")) {
|
||||
// Treat as a raw public key.
|
||||
byte[] pubKey = new BigInteger(destination, 16).toByteArray();
|
||||
ECKey key = ECKey.fromPublicOnly(pubKey);
|
||||
t.addOutput(value, key);
|
||||
OutputSpec outputSpec = new OutputSpec(spec);
|
||||
if (outputSpec.isAddress()) {
|
||||
t.addOutput(outputSpec.value, outputSpec.addr);
|
||||
} else {
|
||||
// Treat as an address.
|
||||
Address addr = Address.fromBase58(params, destination);
|
||||
t.addOutput(value, addr);
|
||||
t.addOutput(outputSpec.value, outputSpec.key);
|
||||
}
|
||||
} catch (WrongNetworkException e) {
|
||||
System.err.println("Malformed output specification, address is for a different network: " + parts[0]);
|
||||
System.err.println("Malformed output specification, address is for a different network: " + spec);
|
||||
return;
|
||||
} catch (AddressFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as address: " + parts[0]);
|
||||
System.err.println("Malformed output specification, could not parse as address: " + spec);
|
||||
return;
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as value: " + parts[1]);
|
||||
System.err.println("Malformed output specification, could not parse as value: " + spec);
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println(e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
Wallet.SendRequest req = Wallet.SendRequest.forTx(t);
|
||||
@ -605,6 +650,335 @@ public class WalletTool {
|
||||
}
|
||||
}
|
||||
|
||||
static class OutputSpec {
|
||||
public final Coin value;
|
||||
public final Address addr;
|
||||
public final ECKey key;
|
||||
|
||||
public OutputSpec(String spec) throws IllegalArgumentException {
|
||||
String[] parts = spec.split(":");
|
||||
if (parts.length != 2) {
|
||||
throw new IllegalArgumentException("Malformed output specification, must have two parts separated by :");
|
||||
}
|
||||
String destination = parts[0];
|
||||
if ("ALL".equalsIgnoreCase(parts[1]))
|
||||
value = wallet.getBalance(BalanceType.ESTIMATED);
|
||||
else
|
||||
value = parseCoin(parts[1]);
|
||||
if (destination.startsWith("0")) {
|
||||
// Treat as a raw public key.
|
||||
byte[] pubKey = new BigInteger(destination, 16).toByteArray();
|
||||
key = ECKey.fromPublicOnly(pubKey);
|
||||
addr = null;
|
||||
} else {
|
||||
// Treat as an address.
|
||||
addr = Address.fromBase58(params, destination);
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAddress() {
|
||||
return addr != null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendCLTVPaymentChannel(String refund, String output, Coin fee, String lockTimeStr, boolean allowUnconfirmed) throws VerificationException {
|
||||
try {
|
||||
// Convert the input strings to outputs.
|
||||
ECKey outputKey, refundKey;
|
||||
Coin value;
|
||||
try {
|
||||
OutputSpec outputSpec = new OutputSpec(output);
|
||||
if (outputSpec.isAddress()) {
|
||||
System.err.println("Output specification must be a public key");
|
||||
return;
|
||||
}
|
||||
outputKey = outputSpec.key;
|
||||
value = outputSpec.value;
|
||||
byte[] refundPubKey = new BigInteger(refund, 16).toByteArray();
|
||||
refundKey = ECKey.fromPublicOnly(refundPubKey);
|
||||
} catch (WrongNetworkException e) {
|
||||
System.err.println("Malformed output specification, address is for a different network.");
|
||||
return;
|
||||
} catch (AddressFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as address.");
|
||||
return;
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as value.");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println(e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
long lockTime;
|
||||
try {
|
||||
lockTime = parseLockTimeStr(lockTimeStr);
|
||||
} catch (ParseException e) {
|
||||
System.err.println("Could not understand --locktime of " + lockTimeStr);
|
||||
return;
|
||||
} catch (ScriptException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Wallet.SendRequest req = Wallet.SendRequest.toCLTVPaymentChannel(params, lockTime, refundKey, outputKey, value);
|
||||
if (req.tx.getOutputs().size() == 1 && req.tx.getOutput(0).getValue().equals(wallet.getBalance())) {
|
||||
log.info("Emptying out wallet, recipient may get less than what you expect");
|
||||
req.emptyWallet = true;
|
||||
}
|
||||
req.fee = fee;
|
||||
if (allowUnconfirmed) {
|
||||
wallet.allowSpendingUnconfirmedTransactions();
|
||||
}
|
||||
if (password != null) {
|
||||
req.aesKey = passwordToKey(true);
|
||||
if (req.aesKey == null)
|
||||
return; // Error message already printed.
|
||||
}
|
||||
wallet.completeTx(req);
|
||||
|
||||
System.out.println(req.tx.getHashAsString());
|
||||
if (options.has("offline")) {
|
||||
wallet.commitTx(req.tx);
|
||||
return;
|
||||
}
|
||||
|
||||
setup();
|
||||
peers.start();
|
||||
// Wait for peers to connect, the tx to be sent to one of them and for it to be propagated across the
|
||||
// network. Once propagation is complete and we heard the transaction back from all our peers, it will
|
||||
// be committed to the wallet.
|
||||
peers.broadcastTransaction(req.tx).future().get();
|
||||
// Hack for regtest/single peer mode, as we're about to shut down and won't get an ACK from the remote end.
|
||||
List<Peer> peerList = peers.getConnectedPeers();
|
||||
if (peerList.size() == 1)
|
||||
peerList.get(0).ping().get();
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyCrypterException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InsufficientMoneyException e) {
|
||||
System.err.println("Insufficient funds: have " + wallet.getBalance().toFriendlyString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settles a CLTV payment channel transaction given that we own both private keys (ie. for testing).
|
||||
* @param txHash
|
||||
* @param output
|
||||
* @param fee
|
||||
* @param allowUnconfirmed
|
||||
*/
|
||||
private static void settleCLTVPaymentChannel(String txHash, String output, Coin fee, boolean allowUnconfirmed) {
|
||||
try {
|
||||
OutputSpec outputSpec;
|
||||
Coin value;
|
||||
try {
|
||||
outputSpec = new OutputSpec(output);
|
||||
value = outputSpec.value;
|
||||
} catch (WrongNetworkException e) {
|
||||
System.err.println("Malformed output specification, address is for a different network.");
|
||||
return;
|
||||
} catch (AddressFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as address.");
|
||||
return;
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as value.");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println(e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
Wallet.SendRequest req = outputSpec.isAddress() ?
|
||||
Wallet.SendRequest.to(outputSpec.addr, value) :
|
||||
Wallet.SendRequest.to(params, outputSpec.key, value);
|
||||
req.fee = fee;
|
||||
|
||||
Transaction lockTimeVerify = wallet.getTransaction(Sha256Hash.wrap(txHash));
|
||||
if (lockTimeVerify == null) {
|
||||
System.err.println("Couldn't find transaction with given hash");
|
||||
return;
|
||||
}
|
||||
TransactionOutput lockTimeVerifyOutput = null;
|
||||
for (TransactionOutput out : lockTimeVerify.getOutputs()) {
|
||||
if (out.getScriptPubKey().isSentToCLTVPaymentChannel()) {
|
||||
lockTimeVerifyOutput = out;
|
||||
}
|
||||
}
|
||||
if (lockTimeVerifyOutput == null) {
|
||||
System.err.println("TX to spend wasn't sent to LockTimeVerify");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.fee.add(value).equals(lockTimeVerifyOutput.getValue())) {
|
||||
System.err.println("You must spend all the money in the input transaction");
|
||||
}
|
||||
|
||||
if (allowUnconfirmed) {
|
||||
wallet.allowSpendingUnconfirmedTransactions();
|
||||
}
|
||||
if (password != null) {
|
||||
req.aesKey = passwordToKey(true);
|
||||
if (req.aesKey == null)
|
||||
return; // Error message already printed.
|
||||
}
|
||||
|
||||
ECKey key1 = wallet.findKeyFromPubKey(
|
||||
lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelSenderPubKey());
|
||||
ECKey key2 = wallet.findKeyFromPubKey(
|
||||
lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelRecipientPubKey());
|
||||
if (key1 == null || key2 == null) {
|
||||
System.err.println("Don't own private keys for both pubkeys");
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionInput input = new TransactionInput(
|
||||
params, req.tx, new byte[] {}, lockTimeVerifyOutput.getOutPointFor());
|
||||
req.tx.addInput(input);
|
||||
TransactionSignature sig1 =
|
||||
req.tx.calculateSignature(0, key1, lockTimeVerifyOutput.getScriptPubKey(), Transaction.SigHash.SINGLE, false);
|
||||
TransactionSignature sig2 =
|
||||
req.tx.calculateSignature(0, key2, lockTimeVerifyOutput.getScriptPubKey(), Transaction.SigHash.SINGLE, false);
|
||||
input.setScriptSig(ScriptBuilder.createCLTVPaymentChannelInput(sig1, sig2));
|
||||
|
||||
System.out.println(req.tx.getHashAsString());
|
||||
if (options.has("offline")) {
|
||||
wallet.commitTx(req.tx);
|
||||
return;
|
||||
}
|
||||
|
||||
setup();
|
||||
peers.start();
|
||||
// Wait for peers to connect, the tx to be sent to one of them and for it to be propagated across the
|
||||
// network. Once propagation is complete and we heard the transaction back from all our peers, it will
|
||||
// be committed to the wallet.
|
||||
peers.broadcastTransaction(req.tx).future().get();
|
||||
// Hack for regtest/single peer mode, as we're about to shut down and won't get an ACK from the remote end.
|
||||
List<Peer> peerList = peers.getConnectedPeers();
|
||||
if (peerList.size() == 1)
|
||||
peerList.get(0).ping().get();
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyCrypterException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds a CLTV payment channel transaction after the lock time has expired.
|
||||
* @param txHash
|
||||
* @param output
|
||||
* @param fee
|
||||
* @param allowUnconfirmed
|
||||
*/
|
||||
private static void refundCLTVPaymentChannel(String txHash, String output, Coin fee, boolean allowUnconfirmed) {
|
||||
try {
|
||||
OutputSpec outputSpec;
|
||||
Coin value;
|
||||
try {
|
||||
outputSpec = new OutputSpec(output);
|
||||
value = outputSpec.value;
|
||||
} catch (WrongNetworkException e) {
|
||||
System.err.println("Malformed output specification, address is for a different network.");
|
||||
return;
|
||||
} catch (AddressFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as address.");
|
||||
return;
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Malformed output specification, could not parse as value.");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println(e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
Wallet.SendRequest req = outputSpec.isAddress() ?
|
||||
Wallet.SendRequest.to(outputSpec.addr, value) :
|
||||
Wallet.SendRequest.to(params, outputSpec.key, value);
|
||||
req.fee = fee;
|
||||
|
||||
Transaction lockTimeVerify = wallet.getTransaction(Sha256Hash.wrap(txHash));
|
||||
if (lockTimeVerify == null) {
|
||||
System.err.println("Couldn't find transaction with given hash");
|
||||
return;
|
||||
}
|
||||
TransactionOutput lockTimeVerifyOutput = null;
|
||||
for (TransactionOutput out : lockTimeVerify.getOutputs()) {
|
||||
if (out.getScriptPubKey().isSentToCLTVPaymentChannel()) {
|
||||
lockTimeVerifyOutput = out;
|
||||
}
|
||||
}
|
||||
if (lockTimeVerifyOutput == null) {
|
||||
System.err.println("TX to spend wasn't sent to LockTimeVerify");
|
||||
return;
|
||||
}
|
||||
|
||||
req.tx.setLockTime(lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelExpiry().longValue());
|
||||
|
||||
if (!req.fee.add(value).equals(lockTimeVerifyOutput.getValue())) {
|
||||
System.err.println("You must spend all the money in the input transaction");
|
||||
}
|
||||
|
||||
if (allowUnconfirmed) {
|
||||
wallet.allowSpendingUnconfirmedTransactions();
|
||||
}
|
||||
if (password != null) {
|
||||
req.aesKey = passwordToKey(true);
|
||||
if (req.aesKey == null)
|
||||
return; // Error message already printed.
|
||||
}
|
||||
|
||||
ECKey key = wallet.findKeyFromPubKey(
|
||||
lockTimeVerifyOutput.getScriptPubKey().getCLTVPaymentChannelSenderPubKey());
|
||||
if (key == null) {
|
||||
System.err.println("Don't own private key for pubkey");
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionInput input = new TransactionInput(
|
||||
params, req.tx, new byte[] {}, lockTimeVerifyOutput.getOutPointFor());
|
||||
input.setSequenceNumber(0);
|
||||
req.tx.addInput(input);
|
||||
TransactionSignature sig =
|
||||
req.tx.calculateSignature(0, key, lockTimeVerifyOutput.getScriptPubKey(), Transaction.SigHash.SINGLE, false);
|
||||
input.setScriptSig(ScriptBuilder.createCLTVPaymentChannelRefund(sig));
|
||||
|
||||
System.out.println(req.tx.getHashAsString());
|
||||
if (options.has("offline")) {
|
||||
wallet.commitTx(req.tx);
|
||||
return;
|
||||
}
|
||||
|
||||
setup();
|
||||
peers.start();
|
||||
// Wait for peers to connect, the tx to be sent to one of them and for it to be propagated across the
|
||||
// network. Once propagation is complete and we heard the transaction back from all our peers, it will
|
||||
// be committed to the wallet.
|
||||
peers.broadcastTransaction(req.tx).future().get();
|
||||
// Hack for regtest/single peer mode, as we're about to shut down and won't get an ACK from the remote end.
|
||||
List<Peer> peerList = peers.getConnectedPeers();
|
||||
if (peerList.size() == 1)
|
||||
peerList.get(0).ping().get();
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyCrypterException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the string either as a whole number of blocks, or if it contains slashes as a YYYY/MM/DD format date
|
||||
* and returns the lock time in wire format.
|
||||
|
@ -62,6 +62,30 @@ Usage: wallet-tool --flags action-name
|
||||
If --date is specified, that's the creation date.
|
||||
If --unixtime is specified, that's the creation time and it overrides --date.
|
||||
If you omit both options, the creation time is being cleared (set to 0).
|
||||
send-cltvpaymentchannel
|
||||
Creates and broadcasts a transaction paying to a CHECKLOCKTIMEVERIFY micropayment channel.
|
||||
Requires a public key for the money recipient, public key to create the transactions (the
|
||||
"return" address) and an expiry time.
|
||||
Options:
|
||||
--output=pubkey:value sets the amount to lock and the recipient
|
||||
--refund-to=pubkey sets "our" public key
|
||||
--fee=value sets the mining fee
|
||||
--locktime=YYYY/MM/DD sets the expiry time for the channel
|
||||
settle-cltvpaymentchannel
|
||||
Creates and broadcasts a transaction settling a previous micropayment channel.
|
||||
This tool, for testing, requires the presence of both private keys.
|
||||
Options:
|
||||
--output=pubkey:value sets the destination for the money
|
||||
--fee=value sets the mining fee
|
||||
--txhash=hash sets the transaction to spend
|
||||
refund-cltvpaymentchannel
|
||||
Creates and broadcasts a transaction refunding a previous micropayment channel.
|
||||
This command can only be called once the expiry for the micropayment channel has passed -
|
||||
the created transaction won't be accepted into the mempool until that point.
|
||||
Options:
|
||||
--output=pubkey:value sets the destination for the money
|
||||
--fee=value sets the mining fee
|
||||
--txhash=hash sets the transaction to spend
|
||||
|
||||
>>> GENERAL OPTIONS
|
||||
--debuglog Enables logging from the core library.
|
||||
|
Loading…
x
Reference in New Issue
Block a user