forked from Qortal/qortal
Unified Bitcoin/Litecoin test apps
This commit is contained in:
parent
2ffd0770c6
commit
af7d7d0966
112
src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java
Normal file
112
src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java
Normal file
@ -0,0 +1,112 @@
|
||||
package org.qortal.test.crosschain.apps;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class BuildHTLC {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: BuildHTLC (-b | -l) <refund-P2PKH> <amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
|
||||
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
|
||||
System.err.println(String.format("example: BuildHTLC -l "
|
||||
+ "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1600000000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 6 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Common.init();
|
||||
|
||||
Bitcoiny bitcoiny = null;
|
||||
NetworkParameters params = null;
|
||||
|
||||
Address refundAddress = null;
|
||||
Coin amount = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
switch (args[argIndex++]) {
|
||||
case "-b":
|
||||
bitcoiny = Bitcoin.getInstance();
|
||||
break;
|
||||
|
||||
case "-l":
|
||||
bitcoiny = Litecoin.getInstance();
|
||||
break;
|
||||
|
||||
default:
|
||||
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
|
||||
}
|
||||
params = bitcoiny.getNetworkParameters();
|
||||
|
||||
refundAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund address must be in P2PKH form");
|
||||
|
||||
amount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
|
||||
if (p2shFee.isZero())
|
||||
return;
|
||||
|
||||
System.out.println(String.format("Refund address: %s", refundAddress));
|
||||
System.out.println(String.format("Amount: %s", amount.toPlainString()));
|
||||
System.out.println(String.format("Redeem address: %s", redeemAddress));
|
||||
System.out.println(String.format("Refund/redeem miner's fee: %s", bitcoiny.format(p2shFee)));
|
||||
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
amount = amount.add(p2shFee);
|
||||
|
||||
// Fund P2SH
|
||||
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
|
||||
p2shAddress, bitcoiny.format(amount), bitcoiny.format(p2shFee)));
|
||||
}
|
||||
|
||||
}
|
133
src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java
Normal file
133
src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java
Normal file
@ -0,0 +1,133 @@
|
||||
package org.qortal.test.crosschain.apps;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class CheckHTLC {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: CheckHTLC (-b | -l) <P2SH-address> <refund-P2PKH> <amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
|
||||
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
|
||||
System.err.println(String.format("example: CheckP2SH -l "
|
||||
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
|
||||
+ "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1600184800"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 7 || args.length > 7)
|
||||
usage(null);
|
||||
|
||||
Common.init();
|
||||
|
||||
Bitcoiny bitcoiny = null;
|
||||
NetworkParameters params = null;
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundAddress = null;
|
||||
Coin amount = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
switch (args[argIndex++]) {
|
||||
case "-b":
|
||||
bitcoiny = Bitcoin.getInstance();
|
||||
break;
|
||||
|
||||
case "-l":
|
||||
bitcoiny = Litecoin.getInstance();
|
||||
break;
|
||||
|
||||
default:
|
||||
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
|
||||
}
|
||||
params = bitcoiny.getNetworkParameters();
|
||||
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund address must be in P2PKH form");
|
||||
|
||||
amount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
|
||||
if (p2shFee.isZero())
|
||||
return;
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Refund PKH: %s", refundAddress));
|
||||
System.out.println(String.format("Redeem/refund amount: %s", amount.toPlainString()));
|
||||
System.out.println(String.format("Redeem PKH: %s", redeemAddress));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
|
||||
System.out.println(String.format("Redeem/refund miner's fee: %s", bitcoiny.format(p2shFee)));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
amount = amount.add(p2shFee);
|
||||
|
||||
// Check network's median block time
|
||||
int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null);
|
||||
if (medianBlockTime == 0)
|
||||
return;
|
||||
|
||||
// Check P2SH is funded
|
||||
Common.getBalance(bitcoiny, p2shAddress.toString());
|
||||
|
||||
// Grab all unspent outputs
|
||||
Common.getUnspentOutputs(bitcoiny, p2shAddress.toString());
|
||||
|
||||
Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), amount.value);
|
||||
}
|
||||
|
||||
}
|
155
src/test/java/org/qortal/test/crosschain/apps/Common.java
Normal file
155
src/test/java/org/qortal/test/crosschain/apps/Common.java
Normal file
@ -0,0 +1,155 @@
|
||||
package org.qortal.test.crosschain.apps;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public abstract class Common {
|
||||
|
||||
public static void init() {
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
}
|
||||
|
||||
public static long getP2shFee(Bitcoiny bitcoiny) {
|
||||
long p2shFee;
|
||||
|
||||
try {
|
||||
p2shFee = bitcoiny.getP2shFee(null);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to determine P2SH fee: %s", e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
|
||||
return p2shFee;
|
||||
}
|
||||
|
||||
public static int checkMedianBlockTime(Bitcoiny bitcoiny, Integer lockTime) {
|
||||
int medianBlockTime;
|
||||
|
||||
try {
|
||||
medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to determine median block time: %s", e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now < medianBlockTime * 1000L) {
|
||||
System.out.println(String.format("Too soon (%s) based on median block time %s",
|
||||
LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC),
|
||||
LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (lockTime != null && now < lockTime * 1000L) {
|
||||
System.err.println(String.format("Too soon (%s) based on lockTime %s",
|
||||
LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC),
|
||||
LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
return medianBlockTime;
|
||||
}
|
||||
|
||||
public static long getBalance(Bitcoiny bitcoiny, String address58) {
|
||||
long balance;
|
||||
|
||||
try {
|
||||
balance = bitcoiny.getConfirmedBalance(address58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to check address %s balance: %s", address58, e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Address %s balance: %s", address58, bitcoiny.format(balance)));
|
||||
|
||||
return balance;
|
||||
}
|
||||
|
||||
public static List<TransactionOutput> getUnspentOutputs(Bitcoiny bitcoiny, String address58) {
|
||||
List<TransactionOutput> unspentOutputs = Collections.emptyList();
|
||||
|
||||
try {
|
||||
unspentOutputs = bitcoiny.getUnspentOutputs(address58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Can't find unspent outputs for %s: %s", address58, e.getMessage()));
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for %s",
|
||||
unspentOutputs.size(),
|
||||
(unspentOutputs.size() != 1 ? "s" : ""),
|
||||
address58));
|
||||
|
||||
for (TransactionOutput fundingOutput : unspentOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s",
|
||||
HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(),
|
||||
bitcoiny.format(fundingOutput.getValue())));
|
||||
|
||||
if (unspentOutputs.isEmpty())
|
||||
System.err.println(String.format("Can't use spent/unfunded %s", address58));
|
||||
|
||||
if (unspentOutputs.size() != 1)
|
||||
System.err.println(String.format("Expecting only one unspent output?"));
|
||||
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
public static BitcoinyHTLC.Status determineHtlcStatus(Bitcoiny bitcoiny, String address58, long minimumAmount) {
|
||||
BitcoinyHTLC.Status htlcStatus = null;
|
||||
|
||||
try {
|
||||
htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), address58, minimumAmount);
|
||||
|
||||
System.out.println(String.format("HTLC status: %s", htlcStatus.name()));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to determine HTLC status: %s", e.getMessage()));
|
||||
}
|
||||
|
||||
return htlcStatus;
|
||||
}
|
||||
|
||||
public static void broadcastTransaction(Bitcoiny bitcoiny, Transaction transaction) {
|
||||
byte[] rawTransactionBytes = transaction.bitcoinSerialize();
|
||||
|
||||
System.out.println(String.format("%nRaw transaction bytes:%n%s%n", HashCode.fromBytes(rawTransactionBytes).toString()));
|
||||
|
||||
for (int countDown = 5; countDown >= 1; --countDown) {
|
||||
System.out.print(String.format("\rBroadcasting transaction in %d second%s... use CTRL-C to abort ", countDown, (countDown != 1 ? "s" : "")));
|
||||
try {
|
||||
Thread.sleep(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
System.out.println("Broadcasting transaction... ");
|
||||
|
||||
try {
|
||||
bitcoiny.broadcastTransaction(transaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Failed to broadcast transaction: %s", e.getMessage()));
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
src/test/java/org/qortal/test/crosschain/apps/Pay.java
Normal file
78
src/test/java/org/qortal/test/crosschain/apps/Pay.java
Normal file
@ -0,0 +1,78 @@
|
||||
package org.qortal.test.crosschain.apps;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
|
||||
public class Pay {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Pay (-b | -l) <xprv58> <recipient> <LTC-amount>"));
|
||||
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
|
||||
System.err.println(String.format("example: Pay -l "
|
||||
+ "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ \\\n"
|
||||
+ "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
|
||||
+ "\t0.00008642"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 4 || args.length > 4)
|
||||
usage(null);
|
||||
|
||||
Common.init();
|
||||
|
||||
Bitcoiny bitcoiny = null;
|
||||
NetworkParameters params = null;
|
||||
|
||||
String xprv58 = null;
|
||||
Address address = null;
|
||||
Coin amount = null;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
switch (args[argIndex++]) {
|
||||
case "-b":
|
||||
bitcoiny = Bitcoin.getInstance();
|
||||
break;
|
||||
|
||||
case "-l":
|
||||
bitcoiny = Litecoin.getInstance();
|
||||
break;
|
||||
|
||||
default:
|
||||
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
|
||||
}
|
||||
params = bitcoiny.getNetworkParameters();
|
||||
|
||||
xprv58 = args[argIndex++];
|
||||
if (!bitcoiny.isValidXprv(xprv58))
|
||||
usage("xprv invalid");
|
||||
|
||||
address = Address.fromString(params, args[argIndex++]);
|
||||
|
||||
amount = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
System.out.println(String.format("Address: %s", address));
|
||||
System.out.println(String.format("Amount: %s", amount.toPlainString()));
|
||||
|
||||
Transaction transaction = bitcoiny.buildSpend(xprv58, address.toString(), amount.value);
|
||||
if (transaction == null) {
|
||||
System.err.println("Insufficent funds");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
Common.broadcastTransaction(bitcoiny, transaction);
|
||||
}
|
||||
|
||||
}
|
164
src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java
Normal file
164
src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java
Normal file
@ -0,0 +1,164 @@
|
||||
package org.qortal.test.crosschain.apps;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class RedeemHTLC {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Redeem (-b | -l) <P2SH-address> <refund-P2PKH> <redeem-PRIVATE-key> <secret> <locktime> <output-address>"));
|
||||
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
|
||||
System.err.println(String.format("example: Redeem -l "
|
||||
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
|
||||
+ "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
|
||||
+ "\tefdaed23c4bc85c8ccae40d774af3c2a10391c648b6420cdd83cd44c27fcb5955201c64e372d \\\n"
|
||||
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
|
||||
+ "\t1600184800 \\\n"
|
||||
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 7 || args.length > 7)
|
||||
usage(null);
|
||||
|
||||
Common.init();
|
||||
|
||||
Bitcoiny bitcoiny = null;
|
||||
NetworkParameters params = null;
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundAddress = null;
|
||||
byte[] redeemPrivateKey = null;
|
||||
byte[] secret = null;
|
||||
int lockTime = 0;
|
||||
Address outputAddress = null;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
switch (args[argIndex++]) {
|
||||
case "-b":
|
||||
bitcoiny = Bitcoin.getInstance();
|
||||
break;
|
||||
|
||||
case "-l":
|
||||
bitcoiny = Litecoin.getInstance();
|
||||
break;
|
||||
|
||||
default:
|
||||
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
|
||||
}
|
||||
params = bitcoiny.getNetworkParameters();
|
||||
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund address must be in P2PKH form");
|
||||
|
||||
redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
|
||||
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
|
||||
if (redeemPrivateKey.length != 32)
|
||||
usage("Redeem private key must be 32 bytes");
|
||||
|
||||
secret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secret.length == 0)
|
||||
usage("Invalid secret bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
outputAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (outputAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Output address invalid");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
|
||||
if (p2shFee.isZero())
|
||||
return;
|
||||
|
||||
System.out.println(String.format("Attempting to redeem HTLC %s to %s", p2shAddress, outputAddress));
|
||||
|
||||
byte[] secretHash = Crypto.hash160(secret);
|
||||
|
||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Actual live processing...
|
||||
|
||||
int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null);
|
||||
if (medianBlockTime == 0)
|
||||
return;
|
||||
|
||||
// Check P2SH is funded
|
||||
long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString());
|
||||
if (p2shBalance == 0)
|
||||
return;
|
||||
|
||||
// Grab all unspent outputs
|
||||
List<TransactionOutput> unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString());
|
||||
if (unspentOutputs.isEmpty())
|
||||
return;
|
||||
|
||||
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
|
||||
|
||||
BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), redeemAmount.value);
|
||||
if (htlcStatus == null)
|
||||
return;
|
||||
|
||||
if (htlcStatus != BitcoinyHTLC.Status.FUNDED) {
|
||||
System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name()));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(redeemAmount), bitcoiny.format(p2shFee)));
|
||||
|
||||
Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
unspentOutputs, redeemScriptBytes, secret, outputAddress.getHash());
|
||||
|
||||
Common.broadcastTransaction(bitcoiny, redeemTransaction);
|
||||
}
|
||||
|
||||
}
|
161
src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java
Normal file
161
src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java
Normal file
@ -0,0 +1,161 @@
|
||||
package org.qortal.test.crosschain.apps;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class RefundHTLC {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: RefundHTLC (-b | -l) <P2SH-address> <refund-PRIVATE-KEY> <redeem-P2PKH> <HASH160-of-secret> <locktime> <output-address>"));
|
||||
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
|
||||
System.err.println(String.format("example: RefundHTLC -l "
|
||||
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
|
||||
+ "\tef8f31b49c31b4a140aebcd9605fded88cc2dad0844c4b984f9191a5a416f72d3801e16447b0 \\\n"
|
||||
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1600184800 \\\n"
|
||||
+ "\tmoJtbbhs7T4Z5hmBH2iyKhGrCWBzQWS2CL"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 7 || args.length > 7)
|
||||
usage(null);
|
||||
|
||||
Common.init();
|
||||
|
||||
Bitcoiny bitcoiny = null;
|
||||
NetworkParameters params = null;
|
||||
|
||||
Address p2shAddress = null;
|
||||
byte[] refundPrivateKey = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Address outputAddress = null;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
switch (args[argIndex++]) {
|
||||
case "-b":
|
||||
bitcoiny = Bitcoin.getInstance();
|
||||
break;
|
||||
|
||||
case "-l":
|
||||
bitcoiny = Litecoin.getInstance();
|
||||
break;
|
||||
|
||||
default:
|
||||
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
|
||||
}
|
||||
params = bitcoiny.getNetworkParameters();
|
||||
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
redeemAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
outputAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (outputAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Output address invalid");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
|
||||
if (p2shFee.isZero())
|
||||
return;
|
||||
|
||||
System.out.println(String.format("Attempting to refund HTLC %s to %s", p2shAddress, outputAddress));
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Actual live processing...
|
||||
|
||||
int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, lockTime);
|
||||
if (medianBlockTime == 0)
|
||||
return;
|
||||
|
||||
// Check P2SH is funded
|
||||
long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString());
|
||||
if (p2shBalance == 0)
|
||||
return;
|
||||
|
||||
// Grab all unspent outputs
|
||||
List<TransactionOutput> unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString());
|
||||
if (unspentOutputs.isEmpty())
|
||||
return;
|
||||
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
|
||||
|
||||
BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), refundAmount.value);
|
||||
if (htlcStatus == null)
|
||||
return;
|
||||
|
||||
if (htlcStatus != BitcoinyHTLC.Status.FUNDED) {
|
||||
System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name()));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(refundAmount), bitcoiny.format(p2shFee)));
|
||||
|
||||
Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||
unspentOutputs, redeemScriptBytes, lockTime, outputAddress.getHash());
|
||||
|
||||
Common.broadcastTransaction(bitcoiny, refundTransaction);
|
||||
}
|
||||
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package org.qortal.test.crosschain.bitcoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class BuildHTLC {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: BuildHTLC <refund-P2PKH> <BTC-amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
|
||||
System.err.println(String.format("example: BuildHTLC "
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 5)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
NetworkParameters params = bitcoin.getNetworkParameters();
|
||||
|
||||
Address refundBitcoinAddress = null;
|
||||
Coin bitcoinAmount = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee;
|
||||
try {
|
||||
p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes);
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
bitcoinAmount = bitcoinAmount.add(p2shFee);
|
||||
|
||||
// Fund P2SH
|
||||
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
|
||||
p2shAddress, bitcoin.format(bitcoinAmount), bitcoin.format(p2shFee)));
|
||||
|
||||
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
|
||||
}
|
||||
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
package org.qortal.test.crosschain.bitcoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class CheckHTLC {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: CheckHTLC <P2SH-address> <refund-P2PKH> <BTC-amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
|
||||
System.err.println(String.format("example: CheckP2SH "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 6 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
NetworkParameters params = bitcoin.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
Coin bitcoinAmount = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee;
|
||||
try {
|
||||
p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Refund Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
bitcoinAmount = bitcoinAmount.add(p2shFee);
|
||||
|
||||
long medianBlockTime = bitcoin.getMedianBlockTime();
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now < medianBlockTime * 1000L)
|
||||
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
// Check P2SH is funded
|
||||
long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (fundingOutputs.size() != 1) {
|
||||
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println("Bitcoin issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package org.qortal.test.crosschain.bitcoinv1;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
public abstract class Common {
|
||||
|
||||
public static final Coin DEFAULT_BTC_FEE = Coin.parseCoin("0.00001000");
|
||||
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
package org.qortal.test.crosschain.bitcoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class Redeem {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Redeem <P2SH-address> <refund-P2PKH> <redeem-PRIVATE-key> <secret> <locktime> "));
|
||||
System.err.println(String.format("example: Redeem "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n"
|
||||
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 5)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
NetworkParameters params = bitcoin.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
byte[] redeemPrivateKey = null;
|
||||
byte[] secret = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
|
||||
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
|
||||
if (redeemPrivateKey.length != 32)
|
||||
usage("Redeem private key must be 32 bytes");
|
||||
|
||||
secret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secret.length == 0)
|
||||
usage("Invalid secret bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee;
|
||||
try {
|
||||
p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee)));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
|
||||
// New/derived info
|
||||
|
||||
byte[] secretHash = Crypto.hash160(secret);
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash())));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Some checks
|
||||
|
||||
System.out.println("\nProcessing:");
|
||||
|
||||
long medianBlockTime;
|
||||
try {
|
||||
medianBlockTime = bitcoin.getMedianBlockTime();
|
||||
} catch (ForeignBlockchainException e1) {
|
||||
System.err.println("Unable to determine median block time");
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now < medianBlockTime * 1000L) {
|
||||
System.err.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
long p2shBalance;
|
||||
try {
|
||||
p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs;
|
||||
try {
|
||||
fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (fundingOutputs.size() != 1) {
|
||||
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||
// No longer fatal
|
||||
}
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoin.format(redeemAmount), bitcoin.format(p2shFee)));
|
||||
|
||||
Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash());
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
|
||||
} catch (NumberFormatException e) {
|
||||
usage(String.format("Number format exception: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
package org.qortal.test.crosschain.bitcoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class Refund {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Refund <P2SH-address> <refund-PRIVATE-KEY> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
|
||||
System.err.println(String.format("example: Refund "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 5)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
NetworkParameters params = bitcoin.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
byte[] refundPrivateKey = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Their BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee;
|
||||
try {
|
||||
p2shFee = Coin.valueOf(bitcoin.getP2shFee(null));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey)));
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Refund miner's fee: %s", bitcoin.format(p2shFee)));
|
||||
|
||||
// New/derived info
|
||||
|
||||
System.out.println("\nCHECKING info from other party:");
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Some checks
|
||||
|
||||
System.out.println("\nProcessing:");
|
||||
|
||||
long medianBlockTime;
|
||||
try {
|
||||
medianBlockTime = bitcoin.getMedianBlockTime();
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println("Unable to determine median block time");
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now < medianBlockTime * 1000L) {
|
||||
System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (now < lockTime * 1000L) {
|
||||
System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC)));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
long p2shBalance;
|
||||
try {
|
||||
p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString());
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs;
|
||||
try {
|
||||
fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString());
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (fundingOutputs.size() != 1) {
|
||||
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||
// No longer fatal
|
||||
}
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoin.format(refundAmount), bitcoin.format(p2shFee)));
|
||||
|
||||
Transaction redeemTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash());
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
|
||||
} catch (NumberFormatException e) {
|
||||
usage(String.format("Number format exception: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package org.qortal.test.crosschain.litecoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class BuildHTLC {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: BuildHTLC <refund-P2PKH> <LTC-amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
|
||||
System.err.println(String.format("example: BuildHTLC "
|
||||
+ "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1600000000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 5)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
NetworkParameters params = litecoin.getNetworkParameters();
|
||||
|
||||
Address refundLitecoinAddress = null;
|
||||
Coin litecoinAmount = null;
|
||||
Address redeemLitecoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
refundLitecoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundLitecoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund Litecoin address must be in P2PKH form");
|
||||
|
||||
litecoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemLitecoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemLitecoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem Litecoin address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee;
|
||||
try {
|
||||
p2shFee = Coin.valueOf(litecoin.getP2shFee(null));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Refund Litecoin address: %s", refundLitecoinAddress));
|
||||
System.out.println(String.format("Litecoin redeem amount: %s", litecoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Litecoin address: %s", redeemLitecoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", litecoin.format(p2shFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundLitecoinAddress.getHash(), lockTime, redeemLitecoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
String p2shAddress = litecoin.deriveP2shAddress(redeemScriptBytes);
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
litecoinAmount = litecoinAmount.add(p2shFee);
|
||||
|
||||
// Fund P2SH
|
||||
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
|
||||
p2shAddress, litecoin.format(litecoinAmount), litecoin.format(p2shFee)));
|
||||
|
||||
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
|
||||
}
|
||||
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
package org.qortal.test.crosschain.litecoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.BitcoinyHTLC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class Refund {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Refund <P2SH-address> <refund-PRIVATE-KEY> <redeem-P2PKH> <HASH160-of-secret> <locktime> <output-address>"));
|
||||
System.err.println(String.format("example: Refund "
|
||||
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
|
||||
+ "\tef8f31b49c31b4a140aebcd9605fded88cc2dad0844c4b984f9191a5a416f72d3801e16447b0 \\\n"
|
||||
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1600184800 \\\n"
|
||||
+ "\tmoJtbbhs7T4Z5hmBH2iyKhGrCWBzQWS2CL"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 6 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Bitcoiny bitcoiny = Litecoin.getInstance();
|
||||
NetworkParameters params = bitcoiny.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
byte[] refundPrivateKey = null;
|
||||
Address redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Address outputAddress = null;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
redeemAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
outputAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (outputAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("output address invalid");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
Coin p2shFee;
|
||||
try {
|
||||
p2shFee = Coin.valueOf(bitcoiny.getP2shFee(null));
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey)));
|
||||
System.out.println(String.format("Redeem address: %s", redeemAddress));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", outputAddress, HashCode.fromBytes(outputAddress.getHash())));
|
||||
|
||||
System.out.println(String.format("Refund miner's fee: %s", bitcoiny.format(p2shFee)));
|
||||
|
||||
// New/derived info
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Some checks
|
||||
|
||||
System.out.println("\nProcessing:");
|
||||
|
||||
long medianBlockTime;
|
||||
try {
|
||||
medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println("Unable to determine median block time");
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now < medianBlockTime * 1000L) {
|
||||
System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (now < lockTime * 1000L) {
|
||||
System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC)));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
long p2shBalance;
|
||||
try {
|
||||
p2shBalance = bitcoiny.getConfirmedBalance(p2shAddress.toString());
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoiny.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs;
|
||||
try {
|
||||
fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString());
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoiny.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (fundingOutputs.size() != 1) {
|
||||
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||
// No longer fatal
|
||||
}
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoiny.format(refundAmount), bitcoiny.format(p2shFee)));
|
||||
|
||||
Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptBytes, lockTime, outputAddress.getHash());
|
||||
|
||||
byte[] rawTransactionBytes = refundTransaction.bitcoinSerialize();
|
||||
|
||||
System.out.println(String.format("\nRaw transaction bytes:\n%s\n", HashCode.fromBytes(rawTransactionBytes).toString()));
|
||||
|
||||
try {
|
||||
bitcoiny.broadcastTransaction(refundTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println(String.format("Failed to broadcast transaction: %s", e.getMessage()));
|
||||
System.exit(1);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
usage(String.format("Number format exception: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package org.qortal.test.crosschain.litecoinv1;
|
||||
|
||||
import java.security.Security;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.Litecoin;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class SendLTC {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: SendLTC <xprv58> <recipient> <LTC-amount>"));
|
||||
System.err.println(String.format("example: SendLTC "
|
||||
+ "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ \\\n"
|
||||
+ "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
|
||||
+ "\t0.00008642"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 3 || args.length > 3)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
NetworkParameters params = litecoin.getNetworkParameters();
|
||||
|
||||
String xprv58 = null;
|
||||
Address litecoinAddress = null;
|
||||
Coin litecoinAmount = null;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
xprv58 = args[argIndex++];
|
||||
if (!litecoin.isValidXprv(xprv58))
|
||||
usage("xprv invalid");
|
||||
|
||||
litecoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
|
||||
litecoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
System.out.println(String.format("Litecoin address: %s", litecoinAddress));
|
||||
System.out.println(String.format("Litecoin amount: %s", litecoinAmount.toPlainString()));
|
||||
|
||||
Transaction transaction = litecoin.buildSpend(xprv58, litecoinAddress.toString(), litecoinAmount.value);
|
||||
if (transaction == null) {
|
||||
System.err.println("Insufficent funds");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
litecoin.broadcastTransaction(transaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
System.err.println("Transaction broadcast failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user