3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-14 11:15:49 +00:00

Initial Litecoin support

Added altcoinj library as Maven dependency.

Added new Litecoin subclass of Bitcoiny,
with mainnet and testnet ElectrumX server lists.

Added litecoinNet settings variable and getter.

Added LitecoinTests.
Most tests work but testFindHtclSecret()
needs a redeemed HTLC on chain (not yet done).

Added litecoinNet to some test settings files
in resources.

Added Litecoin BuildHTLC, Refund test apps.

Added SendLTC app as Electrum-LTC seems a bit flaky?

So far managed to build HTLC P2SH, fund it and then
refund it!

---

As Bitcoin and Litecoin are both subclasses of Bitcoiny,
could unify some test apps with added Bitcoin/Litecoin
switch as first arg?
This commit is contained in:
catbref 2020-09-15 17:53:54 +01:00
parent e3abeafc6b
commit 2ffd0770c6
9 changed files with 706 additions and 1 deletions

14
pom.xml
View File

@ -6,7 +6,8 @@
<version>1.3.5</version>
<packaging>jar</packaging>
<properties>
<bitcoinj.version>0.15.5</bitcoinj.version>
<altcoinj.version>bf9fb80</altcoinj.version>
<bitcoinj.version>0.15.6</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.7</ciyam-at.version>
@ -375,6 +376,11 @@
<name>project</name>
<url>file:${project.basedir}/lib</url>
</repository>
<!-- jitpack for build-on-demand of altcoinj -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.codehaus.mojo/build-helper-maven-plugin -->
@ -415,6 +421,12 @@
<artifactId>bitcoinj-core</artifactId>
<version>${bitcoinj.version}</version>
</dependency>
<!-- For Litecoin, etc. support, requires bitcoinj -->
<dependency>
<groupId>com.github.jjos2372</groupId>
<artifactId>altcoinj</artifactId>
<version>${altcoinj.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>

View File

@ -0,0 +1,166 @@
package org.qortal.crosschain;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.libdohj.params.LitecoinMainNetParams;
import org.libdohj.params.LitecoinRegTestParams;
import org.libdohj.params.LitecoinTestNet3Params;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.settings.Settings;
public class Litecoin extends Bitcoiny {
public static final String CURRENCY_CODE = "LTC";
// Temporary values until a dynamic fee system is written.
private static final long MAINNET_FEE = 1000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
private static final Map<ElectrumX.Server.ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ElectrumX.Server.ConnectionType.class);
static {
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
}
public enum LitecoinNet {
MAIN {
@Override
public NetworkParameters getParams() {
return LitecoinMainNetParams.get();
}
@Override
public Collection<ElectrumX.Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002),
new Server("backup.electrum-ltc.org", Server.ConnectionType.TCP, 50001),
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 50001),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001),
new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022));
}
@Override
public String getGenesisHash() {
return "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2";
}
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return LitecoinTestNet3Params.get();
}
@Override
public Collection<ElectrumX.Server> getServers() {
return Arrays.asList(
new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001),
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 51001),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002));
}
@Override
public String getGenesisHash() {
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return LitecoinRegTestParams.get();
}
@Override
public Collection<ElectrumX.Server> getServers() {
return Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, 50001),
new Server("localhost", Server.ConnectionType.SSL, 50002));
}
@Override
public String getGenesisHash() {
// This is unique to each regtest instance
return null;
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
};
public abstract NetworkParameters getParams();
public abstract Collection<ElectrumX.Server> getServers();
public abstract String getGenesisHash();
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
}
private static Litecoin instance;
private final LitecoinNet litecoinNet;
// Constructors and instance
private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
this.litecoinNet = litecoinNet;
LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name()));
}
public static synchronized Litecoin getInstance() {
if (instance == null) {
LitecoinNet litecoinNet = Settings.getInstance().getLitecoinNet();
BitcoinyBlockchainProvider electrumX = new ElectrumX(litecoinNet.getGenesisHash(), litecoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
Context bitcoinjContext = new Context(litecoinNet.getParams());
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
}
return instance;
}
// Getters & setters
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
/**
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
*/
@Override
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
return this.litecoinNet.getP2shFee(timestamp);
}
}

View File

@ -21,6 +21,7 @@ import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.block.BlockChain;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.Litecoin.LitecoinNet;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@ -98,6 +99,7 @@ public class Settings {
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
@ -370,6 +372,10 @@ public class Settings {
return this.bitcoinNet;
}
public LitecoinNet getLitecoinNet() {
return this.litecoinNet;
}
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}

View File

@ -0,0 +1,115 @@
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class LitecoinTests extends Common {
private Litecoin litecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
litecoin = Litecoin.getInstance();
}
@After
public void afterTest() {
Litecoin.resetForTesting();
litecoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
List<byte[]> rawTransactions = litecoin.getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(litecoin.getNetworkParameters(), p2shAddress, rawTransactions);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = litecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(litecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = litecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(litecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = litecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@ -0,0 +1,112 @@
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");
}
}

View File

@ -0,0 +1,218 @@
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()));
}
}
}

View File

@ -0,0 +1,74 @@
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());
}
}
}

View File

@ -1,5 +1,6 @@
{
"bitcoinNet": "REGTEST",
"litecoinNet": "REGTEST",
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"wipeUnconfirmedOnStart": false,

View File

@ -1,5 +1,6 @@
{
"bitcoinNet": "TEST3",
"litecoinNet": "TEST3",
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"wipeUnconfirmedOnStart": false,