diff --git a/src/main/java/org/qora/crosschain/BTCACCT.java b/src/main/java/org/qora/crosschain/BTCACCT.java
index f6670d44..76a68ee6 100644
--- a/src/main/java/org/qora/crosschain/BTCACCT.java
+++ b/src/main/java/org/qora/crosschain/BTCACCT.java
@@ -3,7 +3,6 @@ package org.qora.crosschain;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
@@ -15,7 +14,6 @@ import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
-import org.bitcoinj.script.Script.ScriptType;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
@@ -28,54 +26,48 @@ public class BTCACCT {
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
- private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes(); // OP_DUP OP_SHA256 push(0x20 bytes)
- private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes(); // OP_EQUAL OP_IF OP_DROP OP_DUP OP_HASH160 push(0x14 bytes)
- private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes(); // OP_EQUALVERIFY OP_CHECKSIG OP_ELSE push(0x4 bytes)
- private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 push(0x14 bytes)
- private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes(); // OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF
+ private static final byte[] redeemScript1 = HashCode.fromString("76a914").asBytes(); // OP_DUP OP_HASH160 push(0x14 bytes)
+ private static final byte[] redeemScript2 = HashCode.fromString("88ada97614").asBytes(); // OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
+ private static final byte[] redeemScript3 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
+ private static final byte[] redeemScript4 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
+ private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
/**
* Returns Bitcoin redeem script.
*
*
- * OP_DUP OP_SHA256 push(0x20) <SHA256 of secret> OP_EQUAL
+ * OP_DUP OP_HASH160 push(0x14) <trade pubkeyhash> OP_EQUALVERIFY OP_CHECKSIGVERIFY
+ * OP_HASH160 OP_DUP push(0x14) <sender/refund P2PKH> OP_EQUAL
* OP_IF
- * OP_DROP OP_DUP OP_HASH160 push(0x14) <HASH160 of recipient pubkey>
- * OP_EQUALVERIFY OP_CHECKSIG
+ * OP_DROP push(0x04 bytes) <refund locktime> OP_CHECKLOCKTIMEVERIFY
* OP_ELSE
- * push(0x04) <refund locktime> OP_CHECKLOCKTIMEVERIFY
- * OP_DROP OP_DUP OP_HASH160 push(0x14) <HASH160 of sender pubkey>
- * OP_EQUALVERIFY OP_CHECKSIG
+ * push(0x14) <redeemer P2PKH> OP_EQUAL
* OP_ENDIF
*
*
- * @param secretHash
+ * @param tradePubKeyHash
* @param senderPubKey
* @param recipientPubKey
* @param lockTime
* @return
*/
- public static byte[] buildRedeemScript(byte[] secretHash, byte[] senderPubKey, byte[] recipientPubKey, int lockTime) {
- byte[] senderPubKeyHash160 = BTC.hash160(senderPubKey);
- byte[] recipientPubKeyHash160 = BTC.hash160(recipientPubKey);
-
- return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash160, redeemScript3, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
- redeemScript4, senderPubKeyHash160, redeemScript5);
+ public static byte[] buildScript(byte[] tradePubKeyHash, byte[] senderPubKeyHash, byte[] recipientPubKeyHash, int lockTime) {
+ return Bytes.concat(redeemScript1, tradePubKeyHash, redeemScript2, senderPubKeyHash, redeemScript3, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
+ redeemScript4, recipientPubKeyHash, redeemScript5);
}
- public static Transaction buildRefundTransaction(Coin refundAmount, ECKey senderKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
+ public static Transaction buildRefundTransaction(Coin refundAmount, ECKey tradeKey, byte[] senderPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction refundTransaction = new Transaction(params);
refundTransaction.setVersion(2);
- refundAmount = refundAmount.subtract(DEFAULT_BTC_FEE);
-
// Output is back to P2SH funder
- refundTransaction.addOutput(refundAmount, ScriptBuilder.createOutputScript(Address.fromKey(params, senderKey, ScriptType.P2PKH)));
+ ECKey senderKey = ECKey.fromPublicOnly(senderPubKey);
+ refundTransaction.addOutput(refundAmount, ScriptBuilder.createP2PKHOutputScript(senderKey));
// Input (without scriptSig prior to signing)
- TransactionInput input = new TransactionInput(params, null, new byte[0], fundingOutput.getOutPointFor());
+ TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
refundTransaction.addInput(input);
@@ -84,27 +76,73 @@ public class BTCACCT {
// Generate transaction signature for input
final boolean anyoneCanPay = false;
- TransactionSignature txSig = refundTransaction.calculateSignature(0, senderKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
+ TransactionSignature txSig = refundTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
+ // sender/refund pubkey
+ scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey));
+
// transaction signature
byte[] txSigBytes = txSig.encodeToBitcoin();
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
- // sender's public key
- byte[] senderPubKey = senderKey.getPubKey();
- scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey));
+ // trade public key
+ byte[] tradePubKey = tradeKey.getPubKey();
+ scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey));
/// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
+ // Set input scriptSig
refundTransaction.getInput(0).setScriptSig(scriptBuilder.build());
return refundTransaction;
}
+ public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey tradeKey, byte[] recipientPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes) {
+ NetworkParameters params = BTC.getInstance().getNetworkParameters();
+
+ Transaction redeemTransaction = new Transaction(params);
+ redeemTransaction.setVersion(2);
+
+ // Output to redeem recipient
+ ECKey senderKey = ECKey.fromPublicOnly(recipientPubKey);
+ redeemTransaction.addOutput(redeemAmount, ScriptBuilder.createP2PKHOutputScript(senderKey));
+
+ // Input (without scriptSig prior to signing)
+ TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
+ input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
+ redeemTransaction.addInput(input);
+
+ // Generate transaction signature for input
+ final boolean anyoneCanPay = false;
+ TransactionSignature txSig = redeemTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
+
+ // Build scriptSig with...
+ ScriptBuilder scriptBuilder = new ScriptBuilder();
+
+ // recipient pubkey
+ scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
+
+ // transaction signature
+ byte[] txSigBytes = txSig.encodeToBitcoin();
+ scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
+
+ // trade public key
+ byte[] tradePubKey = tradeKey.getPubKey();
+ scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey));
+
+ /// redeem script
+ scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
+
+ // Set input scriptSig
+ redeemTransaction.getInput(0).setScriptSig(scriptBuilder.build());
+
+ return redeemTransaction;
+ }
+
public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) {
// Labels for data segment addresses
int addrCounter = 0;
diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java
index d55b84f1..7eaee53b 100644
--- a/src/main/java/org/qortal/crosschain/BTC.java
+++ b/src/main/java/org/qortal/crosschain/BTC.java
@@ -10,6 +10,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
@@ -28,6 +29,7 @@ import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.CheckpointManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
@@ -35,6 +37,7 @@ import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.params.MainNetParams;
+import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
@@ -107,7 +110,7 @@ public class BTC {
return; // Too recent
LOGGER.trace(() -> String.format("Checkpointing at block %d dated %s", height, LocalDateTime.ofInstant(Instant.ofEpochSecond(blockTimestamp), ZoneOffset.UTC)));
- checkpoints.put(blockTimestamp, block);
+ this.checkpoints.put(blockTimestamp, block);
try {
this.saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()));
@@ -121,11 +124,11 @@ public class BTC {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
writer.println("TXT CHECKPOINTS 1");
writer.println("0"); // Number of signatures to read. Do this later.
- writer.println(checkpoints.size());
+ writer.println(this.checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
- for (StoredBlock block : checkpoints.values()) {
+ for (StoredBlock block : this.checkpoints.values()) {
block.serializeCompact(buffer);
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
buffer.position(0);
@@ -145,11 +148,11 @@ public class BTC {
dataOutputStream.writeBytes("CHECKPOINTS 1");
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
digestOutputStream.on(true);
- dataOutputStream.writeInt(checkpoints.size());
+ dataOutputStream.writeInt(this.checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
- for (StoredBlock block : checkpoints.values()) {
+ for (StoredBlock block : this.checkpoints.values()) {
block.serializeCompact(buffer);
dataOutputStream.write(buffer.array());
buffer.position(0);
@@ -165,8 +168,10 @@ public class BTC {
private BTC() {
if (Settings.getInstance().useBitcoinTestNet()) {
- this.params = TestNet3Params.get();
- this.checkpointsFileName = "checkpoints-testnet.txt";
+ this.params = RegTestParams.get();
+ this.checkpointsFileName = "checkpoints-regtest.txt";
+ // TestNet3Params.get();
+ // this.checkpointsFileName = "checkpoints-testnet.txt";
} else {
this.params = MainNetParams.get();
this.checkpointsFileName = "checkpoints.txt";
@@ -231,7 +236,13 @@ public class BTC {
this.peerGroup = new PeerGroup(this.params, this.chain);
this.peerGroup.setUserAgent("qortal", "1.0");
- this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params));
+
+ if (this.params != RegTestParams.get()) {
+ this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params));
+ } else {
+ peerGroup.addAddress(PeerAddress.localhost(this.params));
+ }
+
this.peerGroup.start();
}
@@ -306,6 +317,7 @@ public class BTC {
block = block.getPrev(this.blockStore);
}
+ // Descending, but order shouldn't matter as we're picking median...
latestBlocks.sort((a, b) -> Long.compare(b.getHeader().getTimeSeconds(), a.getHeader().getTimeSeconds()));
return latestBlocks.get(5).getHeader().getTimeSeconds();
diff --git a/src/test/java/org/qora/test/apps/BuildCheckpoints.java b/src/test/java/org/qora/test/apps/BuildCheckpoints.java
new file mode 100644
index 00000000..44869bb7
--- /dev/null
+++ b/src/test/java/org/qora/test/apps/BuildCheckpoints.java
@@ -0,0 +1,69 @@
+package org.qora.test.apps;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.TreeMap;
+
+import org.bitcoinj.core.BlockChain;
+import org.bitcoinj.core.CheckpointManager;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.core.PeerAddress;
+import org.bitcoinj.core.PeerGroup;
+import org.bitcoinj.core.StoredBlock;
+import org.bitcoinj.core.VerificationException;
+import org.bitcoinj.core.listeners.NewBestBlockListener;
+import org.bitcoinj.params.RegTestParams;
+import org.bitcoinj.store.BlockStore;
+import org.bitcoinj.store.MemoryBlockStore;
+
+public class BuildCheckpoints {
+
+ private static final TreeMap checkpoints = new TreeMap<>();
+
+ public static void main(String[] args) throws Exception {
+ final NetworkParameters params = RegTestParams.get();
+
+ final BlockStore store = new MemoryBlockStore(params);
+ final BlockChain chain = new BlockChain(params, store);
+ final PeerGroup peerGroup = new PeerGroup(params, chain);
+
+ final InetAddress ipAddress = InetAddress.getLocalHost();
+ final PeerAddress peerAddress = new PeerAddress(params, ipAddress);
+ peerGroup.addAddress(peerAddress);
+ peerGroup.start();
+
+ chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block));
+
+ peerGroup.downloadBlockChain();
+ peerGroup.stop();
+
+ final File checkpointsFile = new File("regtest-checkpoints");
+ saveAsText(checkpointsFile);
+ }
+
+ private static void saveAsText(File textFile) {
+ try (PrintWriter writer = new PrintWriter(
+ new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
+ writer.println("TXT CHECKPOINTS 1");
+ writer.println("0"); // Number of signatures to read. Do this later.
+ writer.println(checkpoints.size());
+
+ ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
+
+ for (StoredBlock block : checkpoints.values()) {
+ block.serializeCompact(buffer);
+ writer.println(CheckpointManager.BASE64.encode(buffer.array()));
+ buffer.position(0);
+ }
+ } catch (FileNotFoundException e) {
+ return;
+ }
+ }
+
+}
diff --git a/src/test/java/org/qora/test/btcacct/Initiate.java b/src/test/java/org/qora/test/btcacct/Initiate.java
new file mode 100644
index 00000000..d081f4ba
--- /dev/null
+++ b/src/test/java/org/qora/test/btcacct/Initiate.java
@@ -0,0 +1,144 @@
+package org.qora.test.btcacct;
+
+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.AddressFormatException;
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.ECKey;
+import org.bitcoinj.core.LegacyAddress;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.params.RegTestParams;
+import org.bitcoinj.params.TestNet3Params;
+import org.bitcoinj.script.Script.ScriptType;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.qora.controller.Controller;
+import org.qora.crosschain.BTC;
+import org.qora.crosschain.BTCACCT;
+import org.qora.repository.DataException;
+import org.qora.repository.Repository;
+import org.qora.repository.RepositoryFactory;
+import org.qora.repository.RepositoryManager;
+import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
+import org.qora.settings.Settings;
+
+import com.google.common.hash.HashCode;
+
+/**
+ * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT.
+ *
+ * Initiator (wants Qora, has BTC)
+ * Funds BTC P2SH address
+ *
+ * Responder (has Qora, wants BTC)
+ * Builds Qora ACCT AT and funds it with Qora
+ *
+ * Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder
+ *
+ * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT
+ * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?)
+ *
+ * Qora ACCT AT sends its Qora to initiator
+ *
+ */
+
+public class Initiate {
+
+ private static final long REFUND_TIMEOUT = 600L; // seconds
+
+ private static void usage(String error) {
+ if (error != null)
+ System.err.println(error);
+
+ System.err.println(String.format("usage: Initiate ()"));
+ System.err.println(String.format("example: Initiate mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ + "\t0.00008642 \\\n"
+ + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o"));
+ System.exit(1);
+ }
+
+ public static void main(String[] args) {
+ if (args.length < 3 || args.length > 4)
+ usage(null);
+
+ Security.insertProviderAt(new BouncyCastleProvider(), 0);
+ Settings.fileInstance("settings-test.json");
+ NetworkParameters params = RegTestParams.get();
+ // TestNet3Params.get();
+
+ Address yourBitcoinAddress = null;
+ Coin bitcoinAmount = null;
+ Address theirBitcoinAddress = null;
+ Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
+
+ try {
+ int argIndex = 0;
+
+ yourBitcoinAddress = Address.fromString(params, args[argIndex++]);
+ if (yourBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
+ usage("Your BTC address is not in P2PKH form");
+
+ bitcoinAmount = Coin.parseCoin(args[argIndex++]);
+
+ theirBitcoinAddress = Address.fromString(params, args[argIndex++]);
+ if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
+ usage("Their BTC address is not in P2PKH form");
+
+ if (args.length > argIndex)
+ bitcoinFee = Coin.parseCoin(args[argIndex++]);
+ } catch (NumberFormatException | AddressFormatException e) {
+ usage(String.format("Argument format exception: %s", e.getMessage()));
+ }
+
+ try {
+ RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
+ RepositoryManager.setRepositoryFactory(repositoryFactory);
+ } catch (DataException e) {
+ throw new RuntimeException("Repository startup issue: " + e.getMessage());
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ System.out.println("Confirm the following is correct based on the info you've given:");
+
+ System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress));
+ System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
+ System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
+ System.out.println(String.format("Bitcoin redeem fee: %s", bitcoinFee.toPlainString()));
+
+ // New/derived info
+
+ ECKey tradeKey = new ECKey();
+ System.out.println("\nSecret info (DO NOT share with other party):");
+ System.out.println(String.format("Trade private key: %s", HashCode.fromBytes(tradeKey.getPrivKeyBytes())));
+
+ System.out.println("\nGive this info to other party:");
+
+ System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash())));
+
+ int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT);
+ System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
+
+ byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinAddress.getHash(), theirBitcoinAddress.getHash(), lockTime);
+ System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
+
+ byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
+
+ Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
+ System.out.println(String.format("P2SH address: %s", p2shAddress));
+
+ bitcoinAmount = bitcoinAmount.add(bitcoinFee);
+
+ // Fund P2SH
+ System.out.println(String.format("\nYou need to fund %s with %s BTC (includes redeem/refund fee of %s)",
+ p2shAddress.toString(), bitcoinAmount.toPlainString(), bitcoinFee.toPlainString()));
+
+ System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
+ } catch (DataException e) {
+ throw new RuntimeException("Repository issue: " + e.getMessage());
+ }
+ }
+
+}
diff --git a/src/test/java/org/qora/test/btcacct/Initiate1.java b/src/test/java/org/qora/test/btcacct/Initiate1.java
deleted file mode 100644
index 60001176..00000000
--- a/src/test/java/org/qora/test/btcacct/Initiate1.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package org.qora.test.btcacct;
-
-import java.math.BigDecimal;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-
-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.params.TestNet3Params;
-import org.bitcoinj.script.Script.ScriptType;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.qora.account.PrivateKeyAccount;
-import org.qora.account.PublicKeyAccount;
-import org.qora.asset.Asset;
-import org.qora.controller.Controller;
-import org.qora.crosschain.BTC;
-import org.qora.crosschain.BTCACCT;
-import org.qora.crypto.Crypto;
-import org.qora.repository.DataException;
-import org.qora.repository.Repository;
-import org.qora.repository.RepositoryFactory;
-import org.qora.repository.RepositoryManager;
-import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
-import org.qora.utils.Base58;
-
-import com.google.common.hash.HashCode;
-
-/**
- * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT.
- *
- * Initiator (wants Qora, has BTC)
- * Funds BTC P2SH address
- *
- * Responder (has Qora, wants BTC)
- * Builds Qora ACCT AT and funds it with Qora
- *
- * Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder
- *
- * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT
- * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?)
- *
- * Qora ACCT AT sends its Qora to initiator
- *
- */
-
-public class Initiate1 {
-
- private static final long REFUND_TIMEOUT = 600L; // seconds
-
- private static void usage() {
- System.err.println(String.format("usage: Initiate1 "));
- System.err.println(String.format("example: Initiate1 pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj \\\n"
- + "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
- + "\t123 0.00008642 \\\n"
- + "\tJBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte \\\n"
- + "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559"));
- System.exit(1);
- }
-
- public static void main(String[] args) {
- if (args.length != 6)
- usage();
-
- Security.insertProviderAt(new BouncyCastleProvider(), 0);
- NetworkParameters params = TestNet3Params.get();
-
- int argIndex = 0;
- String yourQortPrivKey58 = args[argIndex++];
- String yourBitcoinPubKeyHex = args[argIndex++];
-
- String rawQortAmount = args[argIndex++];
- String rawBitcoinAmount = args[argIndex++];
-
- String theirQortPubKey58 = args[argIndex++];
- String theirBitcoinPubKeyHex = args[argIndex++];
-
- try {
- RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
- RepositoryManager.setRepositoryFactory(repositoryFactory);
- } catch (DataException e) {
- throw new RuntimeException("Repository startup issue: " + e.getMessage());
- }
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- System.out.println("Confirm the following is correct based on the info you've given:");
-
- byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58);
- PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey);
- System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress()));
-
- byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
- ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
- Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
- System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress));
-
- byte[] theirQortPubKey = Base58.decode(theirQortPubKey58);
- PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
- System.out.println(String.format("Their Qortal address: %s", theirQortalAccount.getAddress()));
-
- byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
- ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
- Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
- System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
-
- // Some checks
- BigDecimal qortAmount = new BigDecimal(rawQortAmount).setScale(8);
- BigDecimal yourQortBalance = yourQortalAccount.getConfirmedBalance(Asset.QORT);
- if (yourQortBalance.compareTo(qortAmount) <= 0) {
- System.err.println(String.format("Your QORT balance %s is less than required %s", yourQortBalance.toPlainString(), qortAmount.toPlainString()));
- System.exit(2);
- }
-
- // New/derived info
-
- byte[] secret = new byte[32];
- new SecureRandom().nextBytes(secret);
- System.out.println("\nSecret info (DO NOT share with other party):");
- System.out.println("Secret: " + HashCode.fromBytes(secret).toString());
-
- System.out.println("\nGive this info to other party:");
-
- byte[] secretHash = Crypto.digest(secret);
- System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
-
- int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT);
- System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime));
-
- byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinPubKey, theirBitcoinPubKey, lockTime);
- System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
-
- byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
-
- Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
- System.out.println("P2SH address: " + p2shAddress.toString());
-
- Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount).add(BTCACCT.DEFAULT_BTC_FEE);
-
- // Fund P2SH
- System.out.println(String.format("\nYou need to fund %s with %s BTC (includes redeem/refund fee)", p2shAddress.toString(), bitcoinAmount.toPlainString()));
-
- System.out.println("Once this is done, responder should run Respond2 to check P2SH funding and create AT");
- } catch (NumberFormatException e) {
- usage();
- } catch (DataException e) {
- throw new RuntimeException("Repository issue: " + e.getMessage());
- }
- }
-
-}
diff --git a/src/test/java/org/qora/test/btcacct/Redeem.java b/src/test/java/org/qora/test/btcacct/Redeem.java
new file mode 100644
index 00000000..1ba65bce
--- /dev/null
+++ b/src/test/java/org/qora/test/btcacct/Redeem.java
@@ -0,0 +1,199 @@
+package org.qora.test.btcacct;
+
+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.AddressFormatException;
+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.params.RegTestParams;
+import org.bitcoinj.params.TestNet3Params;
+import org.bitcoinj.script.Script.ScriptType;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.qora.controller.Controller;
+import org.qora.crosschain.BTC;
+import org.qora.crosschain.BTCACCT;
+import org.qora.repository.DataException;
+import org.qora.repository.Repository;
+import org.qora.repository.RepositoryFactory;
+import org.qora.repository.RepositoryManager;
+import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
+import org.qora.settings.Settings;
+
+import com.google.common.hash.HashCode;
+
+/**
+ * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT.
+ *
+ * Initiator (wants Qora, has BTC)
+ * Funds BTC P2SH address
+ *
+ * Responder (has Qora, wants BTC)
+ * Builds Qora ACCT AT and funds it with Qora
+ *
+ * Initiator sends trade private key to Responder.
+ * Responder uses their public key + tx signature + trade pubkey + script as input to BTC P2SH address, releasing BTC amount to responder.
+ *
+ * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT
+ * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?)
+ *
+ * Qora ACCT AT sends its Qora to initiator
+ *
+ */
+
+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 final long REFUND_TIMEOUT = 600L; // seconds
+
+ private static void usage(String error) {
+ if (error != null)
+ System.err.println(error);
+
+ System.err.println(String.format("usage: Redeem ()"));
+ System.err.println(String.format("example: Redeem 032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
+ + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ + "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL"));
+ System.exit(1);
+ }
+
+ public static void main(String[] args) {
+ if (args.length < 5 || args.length > 6)
+ usage(null);
+
+ Security.insertProviderAt(new BouncyCastleProvider(), 0);
+ Settings.fileInstance("settings-test.json");
+ NetworkParameters params = RegTestParams.get();
+ // TestNet3Params.get();
+
+ ECKey yourBitcoinKey = null;
+ Address theirBitcoinAddress = null;
+ byte[] tradePrivateKey = null;
+ int lockTime = 0;
+ Address p2shAddress = null;
+ Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
+
+ try {
+ int argIndex = 0;
+
+ yourBitcoinKey = ECKey.fromPublicOnly(HashCode.fromString(args[argIndex++]).asBytes());
+
+ theirBitcoinAddress = Address.fromString(params, args[argIndex++]);
+ if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
+ usage("Their BTC address is not in P2PKH form");
+
+ tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
+ if (tradePrivateKey.length != 32)
+ usage("Trade private key not 32 bytes");
+
+ lockTime = Integer.parseInt(args[argIndex++]);
+
+ p2shAddress = Address.fromString(params, args[argIndex++]);
+ if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
+ usage("P2SH address invalid");
+
+ if (args.length > argIndex)
+ bitcoinFee = Coin.parseCoin(args[argIndex++]);
+ } catch (NumberFormatException | AddressFormatException e) {
+ usage(String.format("Argument format exception: %s", e.getMessage()));
+ }
+
+ try {
+ RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
+ RepositoryManager.setRepositoryFactory(repositoryFactory);
+ } catch (DataException e) {
+ throw new RuntimeException("Repository startup issue: " + e.getMessage());
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ System.out.println("Confirm the following is correct based on the info you've given:");
+
+ System.out.println(String.format("Your Bitcoin address: %s", Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH)));
+ System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
+ System.out.println(String.format("Trade PRIVATE key: %s", HashCode.fromBytes(tradePrivateKey)));
+ 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("Bitcoin redeem fee: %s", bitcoinFee.toPlainString()));
+
+ // New/derived info
+
+ System.out.println("\nCHECKING info from other party:");
+
+ ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey);
+ System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash())));
+
+ byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), theirBitcoinAddress.getHash(), yourBitcoinKey.getPubKeyHash(), lockTime);
+ System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
+
+ byte[] redeemScriptHash = BTC.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 = BTC.getInstance().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.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);
+ }
+
+ Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
+ if (p2shBalance == null) {
+ System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
+ System.exit(2);
+ }
+ System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
+
+ // Grab all P2SH funding transactions (just in case there are more than one)
+ List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
+ System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
+
+ 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);
+ }
+
+ TransactionOutput fundingOutput = fundingOutputs.get(0);
+ System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
+
+ Coin redeemAmount = p2shBalance.subtract(bitcoinFee);
+ Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, tradeKey, yourBitcoinKey.getPubKey(), fundingOutput, redeemScriptBytes);
+
+ 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()));
+ } catch (DataException e) {
+ throw new RuntimeException("Repository issue: " + e.getMessage());
+ }
+ }
+
+}
diff --git a/src/test/java/org/qora/test/btcacct/Refund2.java b/src/test/java/org/qora/test/btcacct/Refund.java
similarity index 57%
rename from src/test/java/org/qora/test/btcacct/Refund2.java
rename to src/test/java/org/qora/test/btcacct/Refund.java
index 05801775..69cddde7 100644
--- a/src/test/java/org/qora/test/btcacct/Refund2.java
+++ b/src/test/java/org/qora/test/btcacct/Refund.java
@@ -3,16 +3,18 @@ package org.qora.test.btcacct;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
-import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.util.List;
import org.bitcoinj.core.Address;
+import org.bitcoinj.core.AddressFormatException;
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.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -46,7 +48,7 @@ import com.google.common.hash.HashCode;
*
*/
-public class Refund2 {
+public class Refund {
static {
// This must go before any calls to LogManager/Logger
@@ -55,31 +57,57 @@ public class Refund2 {
private static final long REFUND_TIMEOUT = 600L; // seconds
- private static void usage() {
- System.err.println(String.format("usage: Refund2 "));
- System.err.println(String.format("example: Refund2 027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c \\\n"
- + "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
- + "\tb837056cdc5d805e4db1f830a58158e1131ac96ea71de4c6f9d7854985e153e2 1575021641 2MvGdGUgAfc7qTHaZJwWmZ26Fg6Hjif8gNy"));
+ private static void usage(String error) {
+ if (error != null)
+ System.err.println(error);
+
+ System.err.println(String.format("usage: Refund ()"));
+ System.err.println(String.format("example: Refund 03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
+ + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ + "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL"));
System.exit(1);
}
public static void main(String[] args) {
- if (args.length != 5)
- usage();
+ if (args.length < 5 || args.length > 6)
+ usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
-
Settings.fileInstance("settings-test.json");
+ NetworkParameters params = RegTestParams.get();
+ // TestNet3Params.get();
- NetworkParameters params = TestNet3Params.get();
+ ECKey yourBitcoinKey = null;
+ Address theirBitcoinAddress = null;
+ byte[] tradePrivateKey = null;
+ int lockTime = 0;
+ Address p2shAddress = null;
+ Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
- int argIndex = 0;
- String yourBitcoinPrivKeyHex = args[argIndex++];
- String theirBitcoinPubKeyHex = args[argIndex++];
+ try {
+ int argIndex = 0;
- String secretHashHex = args[argIndex++];
- String rawLockTime = args[argIndex++];
- String rawP2shAddress = args[argIndex++];
+ yourBitcoinKey = ECKey.fromPublicOnly(HashCode.fromString(args[argIndex++]).asBytes());
+
+ theirBitcoinAddress = Address.fromString(params, args[argIndex++]);
+ if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
+ usage("Their BTC address is not in P2PKH form");
+
+ tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
+ if (tradePrivateKey.length != 32)
+ usage("Trade private key not 32 bytes");
+
+ lockTime = Integer.parseInt(args[argIndex++]);
+
+ p2shAddress = Address.fromString(params, args[argIndex++]);
+ if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
+ usage("P2SH address invalid");
+
+ if (args.length > argIndex)
+ bitcoinFee = Coin.parseCoin(args[argIndex++]);
+ } catch (NumberFormatException | AddressFormatException e) {
+ usage(String.format("Argument format exception: %s", e.getMessage()));
+ }
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
@@ -91,50 +119,47 @@ public class Refund2 {
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Confirm the following is correct based on the info you've given:");
- byte[] yourBitcoinPrivKey = HashCode.fromString(yourBitcoinPrivKeyHex).asBytes();
- ECKey yourBitcoinKey = ECKey.fromPrivate(yourBitcoinPrivKey);
- Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
- System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress));
-
- byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
- ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
- Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
+ System.out.println(String.format("Your Bitcoin address: %s", Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH)));
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
+ System.out.println(String.format("Trade PRIVATE key: %s", HashCode.fromBytes(tradePrivateKey)));
+ 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("Bitcoin redeem fee: %s", bitcoinFee.toPlainString()));
// New/derived info
- int lockTime = Integer.valueOf(rawLockTime);
- System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime));
+ System.out.println("\nCHECKING info from other party:");
- byte[] secretHash = HashCode.fromString(secretHashHex).asBytes();
- System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
+ ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey);
+ System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash())));
- byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinKey.getPubKey(), theirBitcoinPubKey, lockTime);
- System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
+ byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinKey.getPubKeyHash(), theirBitcoinAddress.getHash(), lockTime);
+ System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
+ Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
- Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
- System.out.println(String.format("P2SH address: %s", p2shAddress));
-
- if (!p2shAddress.toString().equals(rawP2shAddress)) {
- System.err.println(String.format("Derived P2SH address %s does not match given address %s", p2shAddress, rawP2shAddress));
+ 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 = BTC.getInstance().getMedianBlockTime();
- System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneId.systemDefault())));
+ 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), ZoneId.systemDefault()), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneId.systemDefault())));
+ 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), ZoneId.systemDefault()), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault())));
+ 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);
}
@@ -159,13 +184,17 @@ public class Refund2 {
System.exit(2);
}
- Transaction refundTransaction = BTCACCT.buildRefundTransaction(p2shBalance, yourBitcoinKey, fundingOutputs.get(0), redeemScriptBytes, lockTime);
+ TransactionOutput fundingOutput = fundingOutputs.get(0);
+ System.out.println(String.format("Using output %s:%d for refund", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
+
+ Coin refundAmount = p2shBalance.subtract(bitcoinFee);
+ Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, tradeKey, yourBitcoinKey.getPubKey(), fundingOutput, redeemScriptBytes, lockTime);
byte[] refundBytes = refundTransaction.bitcoinSerialize();
- System.out.println(String.format("\nLoad this transaction into your wallet, sign and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString()));
+ System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString()));
} catch (NumberFormatException e) {
- usage();
+ usage(String.format("Number format exception: %s", e.getMessage()));
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
}
diff --git a/src/test/java/org/qora/test/btcacct/Respond2.java b/src/test/java/org/qora/test/btcacct/Respond2.java
index 9dac26ed..5699ad1a 100644
--- a/src/test/java/org/qora/test/btcacct/Respond2.java
+++ b/src/test/java/org/qora/test/btcacct/Respond2.java
@@ -61,7 +61,7 @@ public class Respond2 {
private static final long REFUND_TIMEOUT = 600L; // seconds
private static void usage() {
- System.err.println(String.format("usage: Respond2 "));
+ System.err.println(String.format("usage: Respond2 "));
System.err.println(String.format("example: Respond2 3jjoToDaDpsdUHqaouLGypFeewNVKvtkmdM38i54WVra \\\n"
+ "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
+ "\t123 0.00008642 \\\n"
@@ -136,7 +136,7 @@ public class Respond2 {
byte[] secretHash = HashCode.fromString(secretHashHex).asBytes();
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
- byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime);
+ byte[] redeemScriptBytes = BTCACCT.buildScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime);
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);