mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-23 04:36:50 +00:00
Merge branch 'BTC-ACCT' into launch
This commit is contained in:
@@ -70,11 +70,11 @@ public class BTCACCTTests {
|
||||
// For when we want to re-run
|
||||
private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes();
|
||||
private static final long prevLockTime = 1539347892L;
|
||||
private static final boolean usePreviousFundingTx = true;
|
||||
private static final boolean usePreviousFundingTx = false;
|
||||
|
||||
private static final boolean doRefundNotRedeem = false;
|
||||
|
||||
public void main(String[] args) throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException {
|
||||
public static void main(String[] args) throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException {
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
|
||||
byte[] secret = new byte[32];
|
||||
@@ -173,7 +173,7 @@ public class BTCACCTTests {
|
||||
private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes();
|
||||
private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes();
|
||||
|
||||
private byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) {
|
||||
private static byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) {
|
||||
try {
|
||||
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
@@ -188,7 +188,7 @@ public class BTCACCTTests {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] hash160(byte[] input) {
|
||||
private static byte[] hash160(byte[] input) {
|
||||
try {
|
||||
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
|
||||
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
|
||||
@@ -199,7 +199,7 @@ public class BTCACCTTests {
|
||||
}
|
||||
}
|
||||
|
||||
private Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value,
|
||||
private static Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value,
|
||||
byte[] redeemScriptHash) {
|
||||
Transaction fundingTransaction = new Transaction(params);
|
||||
|
||||
@@ -218,7 +218,7 @@ public class BTCACCTTests {
|
||||
return fundingTransaction;
|
||||
}
|
||||
|
||||
private Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret,
|
||||
private static Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret,
|
||||
byte[] redeemScriptBytes) {
|
||||
Transaction redeemTransaction = new Transaction(params);
|
||||
redeemTransaction.setVersion(2);
|
||||
@@ -255,7 +255,7 @@ public class BTCACCTTests {
|
||||
return redeemTransaction;
|
||||
}
|
||||
|
||||
private Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value,
|
||||
private static Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value,
|
||||
byte[] redeemScriptBytes, long lockTime) {
|
||||
Transaction refundTransaction = new Transaction(params);
|
||||
refundTransaction.setVersion(2);
|
||||
@@ -294,7 +294,7 @@ public class BTCACCTTests {
|
||||
return refundTransaction;
|
||||
}
|
||||
|
||||
private void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) {
|
||||
private static void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) {
|
||||
System.out.println("Broadcasting tx: " + transaction.getTxId().toString());
|
||||
System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString());
|
||||
|
||||
@@ -320,7 +320,7 @@ public class BTCACCTTests {
|
||||
}
|
||||
|
||||
/** Convert int to little-endian byte array */
|
||||
private byte[] toLEByteArray(int value) {
|
||||
private static byte[] toLEByteArray(int value) {
|
||||
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
|
||||
}
|
||||
|
||||
|
67
src/test/java/org/qortal/test/apps/BuildCheckpoints.java
Normal file
67
src/test/java/org/qortal/test/apps/BuildCheckpoints.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package org.qortal.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.params.RegTestParams;
|
||||
import org.bitcoinj.store.BlockStore;
|
||||
import org.bitcoinj.store.MemoryBlockStore;
|
||||
|
||||
public class BuildCheckpoints {
|
||||
|
||||
private static final TreeMap<Integer, StoredBlock> 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.getLoopbackAddress();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -13,17 +13,23 @@ import org.qortal.settings.Settings;
|
||||
public class orphan {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length == 0) {
|
||||
System.err.println("usage: orphan <new-blockchain-tip-height>");
|
||||
if (args.length < 1 || args.length > 2) {
|
||||
System.err.println("usage: orphan [<settings-file>] <new-blockchain-tip-height>");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
int targetHeight = Integer.parseInt(args[0]);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
Settings.getInstance();
|
||||
int argIndex = 0;
|
||||
|
||||
if (args.length > 1) {
|
||||
Settings.fileInstance(args[argIndex++]);
|
||||
} else {
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
Settings.getInstance();
|
||||
}
|
||||
|
||||
int targetHeight = Integer.parseInt(args[argIndex]);
|
||||
|
||||
try {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
|
||||
|
552
src/test/java/org/qortal/test/btcacct/AtTests.java
Normal file
552
src/test/java/org/qortal/test/btcacct/AtTests.java
Normal file
@@ -0,0 +1,552 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
public class AtTests extends Common {
|
||||
|
||||
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final int refundTimeout = 10; // blocks
|
||||
public static final long initialPayout = 100000L;
|
||||
public static final long redeemAmount = 80_40200000L;
|
||||
public static final long fundingAmount = 123_45600000L;
|
||||
public static final long bitcoinAmount = 864200L;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
Account deployer = Common.getTestAccount(null, "chloe");
|
||||
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = fundingAmount;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = deployersInitialBalance;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = 0;
|
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// Refund should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testInitialPayment() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT BUT NOT FROM AT CREATOR
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress);
|
||||
|
||||
// Initial payment should NOT happen
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT
|
||||
messageTransaction = sendMessage(repository, recipient, secret, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageTransaction = sendMessage(repository, bystander, secret, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
byte[] wrongSecret = Crypto.digest(secret);
|
||||
messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
for (ATData atData : executableAts) {
|
||||
String atAddress = atData.getATAddress();
|
||||
byte[] codeBytes = atData.getCodeBytes();
|
||||
byte[] codeHash = Crypto.digest(codeBytes);
|
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
|
||||
atAddress,
|
||||
codeBytes.length,
|
||||
(codeBytes.length != 1 ? "s": ""),
|
||||
HashCode.fromBytes(codeHash)));
|
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH))
|
||||
continue;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "QORT-BTC cross-chain trade";
|
||||
String description = String.format("Qortal-Bitcoin cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC ACCT";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = sender.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
long amount = 0;
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, Asset.QORT, amount, data, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - initialPayout;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
}
|
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tHASH160 of secret: %s,\n"
|
||||
+ "\tinitial payout: %s QORT,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected bitcoin: %s BTC,\n"
|
||||
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.initialPayout),
|
||||
Amounts.prettyAmount(tradeData.redeemPayout),
|
||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||
tradeData.tradeRefundTimeout,
|
||||
currentBlockHeight));
|
||||
|
||||
// Are we in 'offer' or 'trade' stage?
|
||||
if (tradeData.tradeRefundHeight == null) {
|
||||
// Offer
|
||||
System.out.println(String.format("\tstatus: 'offer mode'"));
|
||||
} else {
|
||||
// Trade
|
||||
System.out.println(String.format("\tstatus: 'trade mode',\n"
|
||||
+ "\ttrade timeout: block %d,\n"
|
||||
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
|
||||
+ "\ttrade recipient: %s",
|
||||
tradeData.tradeRefundHeight,
|
||||
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L),
|
||||
tradeData.qortalRecipient));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
65
src/test/java/org/qortal/test/btcacct/BtcTests.java
Normal file
65
src/test/java/org/qortal/test/btcacct/BtcTests.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.bitcoinj.wallet.WalletTransaction;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
public class BtcTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException {
|
||||
System.out.println(String.format("Starting BTC instance..."));
|
||||
BTC btc = BTC.getInstance();
|
||||
System.out.println(String.format("BTC instance started"));
|
||||
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", btc.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 testFindP2shSecret() {
|
||||
// This actually exists on TEST3
|
||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
int startTime = 1587510000;
|
||||
|
||||
List<WalletTransaction> walletTransactions = new ArrayList<>();
|
||||
|
||||
BTC.getInstance().getBalanceAndOtherInfo(p2shAddress, startTime, null, walletTransactions);
|
||||
|
||||
byte[] expectedSecret = AtTests.secret;
|
||||
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, walletTransactions);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
}
|
125
src/test/java/org/qortal/test/btcacct/BuildP2SH.java
Normal file
125
src/test/java/org/qortal/test/btcacct/BuildP2SH.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package org.qortal.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.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class BuildP2SH {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: BuildP2SH <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: BuildP2SH "
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address refundBitcoinAddress = null;
|
||||
Coin bitcoinAmount = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, 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("Refund Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
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 = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
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 (includes redeem/refund fee of %s)",
|
||||
p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
172
src/test/java/org/qortal/test/btcacct/CheckP2SH.java
Normal file
172
src/test/java/org/qortal/test/btcacct/CheckP2SH.java
Normal file
@@ -0,0 +1,172 @@
|
||||
package org.qortal.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.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class CheckP2SH {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: CheckP2SH <P2SH-address> <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: CheckP2SH "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 6 || args.length > 7)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
Coin bitcoinAmount = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now");
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, 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("Refund Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
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);
|
||||
}
|
||||
|
||||
bitcoinAmount = bitcoinAmount.add(bitcoinFee);
|
||||
|
||||
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.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
// Check P2SH is funded
|
||||
final int startTime = lockTime - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
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", p2shAddress, BTC.FORMAT.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (fundingOutputs.size() != 1) {
|
||||
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
9
src/test/java/org/qortal/test/btcacct/Common.java
Normal file
9
src/test/java/org/qortal/test/btcacct/Common.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
public abstract class Common {
|
||||
|
||||
public static final Coin DEFAULT_BTC_FEE = Coin.parseCoin("0.00001000");
|
||||
|
||||
}
|
160
src/test/java/org/qortal/test/btcacct/DeployAT.java
Normal file
160
src/test/java/org/qortal/test/btcacct/DeployAT.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import java.security.Security;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class DeployAT {
|
||||
|
||||
public static final long atFundingExtra = 2000000L;
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
|
||||
System.err.println(String.format("example: DeployAT "
|
||||
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
||||
+ "\t80.4020 \\\n"
|
||||
+ "\t0.00864200 \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t0.0001 \\\n"
|
||||
+ "\t123.456 \\\n"
|
||||
+ "\t10"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 8)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
byte[] refundPrivateKey = null;
|
||||
long redeemAmount = 0;
|
||||
long expectedBitcoin = 0;
|
||||
byte[] secretHash = null;
|
||||
long initialPayout = 0;
|
||||
long fundingAmount = 0;
|
||||
int tradeTimeout = 0;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
refundPrivateKey = Base58.decode(args[argIndex++]);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
redeemAmount = Long.parseLong(args[argIndex++]);
|
||||
if (redeemAmount <= 0)
|
||||
usage("QORT amount must be positive");
|
||||
|
||||
expectedBitcoin = Long.parseLong(args[argIndex++]);
|
||||
if (expectedBitcoin <= 0)
|
||||
usage("Expected BTC amount must be positive");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
initialPayout = Long.parseLong(args[argIndex++]);
|
||||
if (initialPayout < 0)
|
||||
usage("Initial QORT payout must be positive");
|
||||
|
||||
fundingAmount = Long.parseLong(args[argIndex++]);
|
||||
if (fundingAmount <= redeemAmount)
|
||||
usage("AT funding amount must be greater than QORT redeem amount");
|
||||
|
||||
tradeTimeout = Integer.parseInt(args[argIndex++]);
|
||||
if (tradeTimeout < 10 || tradeTimeout > 50000)
|
||||
usage("AT trade timeout should be between 10 and 50,000 minutes");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, 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:");
|
||||
|
||||
PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey);
|
||||
System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress()));
|
||||
|
||||
System.out.println(String.format("QORT redeem amount: %s", Amounts.prettyAmount(redeemAmount)));
|
||||
|
||||
System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount)));
|
||||
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
// Deploy AT
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = refundAccount.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", refundAccount.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "QORT-BTC cross-chain trade";
|
||||
String description = String.format("Qortal-Bitcoin cross-chain trade");
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC ACCT";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
deployAtTransaction.sign(refundAccount);
|
||||
|
||||
byte[] signedBytes = null;
|
||||
try {
|
||||
signedBytes = TransactionTransformer.toBytes(deployAtTransactionData);
|
||||
} catch (TransformationException e) {
|
||||
System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("\nSigned transaction in base58, ready for POST /transactions/process:\n%s\n", Base58.encode(signedBytes)));
|
||||
} catch (NumberFormatException e) {
|
||||
usage(String.format("Number format exception: %s", e.getMessage()));
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
64
src/test/java/org/qortal/test/btcacct/GetTransaction.java
Normal file
64
src/test/java/org/qortal/test/btcacct/GetTransaction.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class GetTransaction {
|
||||
|
||||
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: GetTransaction <bitcoin-tx> <start-time>"));
|
||||
System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000"));
|
||||
System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 2 || args.length > 2)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
byte[] transactionId = null;
|
||||
int startTime = 0;
|
||||
|
||||
try {
|
||||
int argIndex = 0;
|
||||
|
||||
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
|
||||
startTime = Integer.parseInt(args[argIndex++]);
|
||||
} catch (NumberFormatException | AddressFormatException e) {
|
||||
usage(String.format("Argument format exception: %s", e.getMessage()));
|
||||
}
|
||||
|
||||
// Grab all outputs from transaction
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.out.println(String.format("Transaction not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %d: %s", fundingOutput.getIndex(), fundingOutput.getValue().toPlainString()));
|
||||
}
|
||||
|
||||
}
|
198
src/test/java/org/qortal/test/btcacct/Redeem.java
Normal file
198
src/test/java/org/qortal/test/btcacct/Redeem.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
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.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class Redeem {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Redeem <P2SH-address> <refund-BTC-P2PKH> <redeem-BTC-PRIVATE-key> <secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: Redeem "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n"
|
||||
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
byte[] redeemPrivateKey = null;
|
||||
byte[] secret = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
|
||||
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
|
||||
if (redeemPrivateKey.length != 32)
|
||||
usage("Redeem private key must be 32 bytes");
|
||||
|
||||
secret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secret.length == 0)
|
||||
usage("Invalid secret bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, 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("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
|
||||
// New/derived info
|
||||
|
||||
byte[] secretHash = BTC.hash160(secret);
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash())));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
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);
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
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<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
if (fundingOutputs.size() != 1) {
|
||||
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
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);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, secret);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
202
src/test/java/org/qortal/test/btcacct/Refund.java
Normal file
202
src/test/java/org/qortal/test/btcacct/Refund.java
Normal file
@@ -0,0 +1,202 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
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.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
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-BTC-PRIVATE-KEY> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: Refund "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
byte[] refundPrivateKey = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Their BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, 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("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey)));
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
// New/derived info
|
||||
|
||||
System.out.println("\nCHECKING info from other party:");
|
||||
|
||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
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 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
|
||||
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
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<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.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"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
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);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -15,9 +15,9 @@ public class BlockUtils {
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class);
|
||||
|
||||
/** Mints a new block using "alice-reward-share" test account. */
|
||||
public static void mintBlock(Repository repository) throws DataException {
|
||||
public static Block mintBlock(Repository repository) throws DataException {
|
||||
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
|
||||
BlockMinter.mintTestingBlock(repository, mintingAccount);
|
||||
return BlockMinter.mintTestingBlock(repository, mintingAccount);
|
||||
}
|
||||
|
||||
public static Long getNextBlockReward(Repository repository) throws DataException {
|
||||
|
@@ -29,6 +29,12 @@
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"ciyamAtSettings": {
|
||||
"feePerStep": "0.0001",
|
||||
"maxStepsPerRound": 500,
|
||||
"stepsPerFunctionCall": 10,
|
||||
"minutesPerBlock": 1
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
|
@@ -29,6 +29,12 @@
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"ciyamAtSettings": {
|
||||
"feePerStep": "0.0001",
|
||||
"maxStepsPerRound": 500,
|
||||
"stepsPerFunctionCall": 10,
|
||||
"minutesPerBlock": 1
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
|
@@ -29,6 +29,12 @@
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"ciyamAtSettings": {
|
||||
"feePerStep": "0.0001",
|
||||
"maxStepsPerRound": 500,
|
||||
"stepsPerFunctionCall": 10,
|
||||
"minutesPerBlock": 1
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
|
@@ -29,6 +29,12 @@
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"ciyamAtSettings": {
|
||||
"feePerStep": "0.0001",
|
||||
"maxStepsPerRound": 500,
|
||||
"stepsPerFunctionCall": 10,
|
||||
"minutesPerBlock": 1
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
|
@@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-minting.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"bitcoinNet": "TEST3",
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
|
Reference in New Issue
Block a user