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:
parent
e3abeafc6b
commit
2ffd0770c6
14
pom.xml
14
pom.xml
@ -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>
|
||||
|
166
src/main/java/org/qortal/crosschain/Litecoin.java
Normal file
166
src/main/java/org/qortal/crosschain/Litecoin.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
115
src/test/java/org/qortal/test/crosschain/LitecoinTests.java
Normal file
115
src/test/java/org/qortal/test/crosschain/LitecoinTests.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
218
src/test/java/org/qortal/test/crosschain/litecoinv1/Refund.java
Normal file
218
src/test/java/org/qortal/test/crosschain/litecoinv1/Refund.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"bitcoinNet": "REGTEST",
|
||||
"litecoinNet": "REGTEST",
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"bitcoinNet": "TEST3",
|
||||
"litecoinNet": "TEST3",
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user