diff --git a/src/test/java/org/qortal/test/crosschain/ACCTTests.java b/src/test/java/org/qortal/test/crosschain/ACCTTests.java
new file mode 100644
index 00000000..6af27a96
--- /dev/null
+++ b/src/test/java/org/qortal/test/crosschain/ACCTTests.java
@@ -0,0 +1,790 @@
+package org.qortal.test.crosschain;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
+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.block.Block;
+import org.qortal.crosschain.ACCT;
+import org.qortal.crosschain.AcctMode;
+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 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.Random;
+import java.util.function.Function;
+
+import static org.junit.Assert.*;
+
+public abstract class ACCTTests extends Common {
+
+	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
+	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
+	public static final int tradeTimeout = 20; // blocks
+	public static final long redeemAmount = 80_40200000L;
+	public static final long fundingAmount = 123_45600000L;
+	public static final long foreignAmount = 864200L; // 0.00864200 foreign units
+
+	protected static final Random RANDOM = new Random();
+
+	protected abstract byte[] getPublicKey();
+
+	protected abstract byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout);
+
+	protected abstract ACCT getInstance();
+
+	protected abstract int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA);
+
+	protected abstract byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout);
+
+	protected abstract byte[] buildRedeemMessage(byte[] secretA, String address);
+
+	protected abstract byte[] getCodeBytesHash();
+
+	protected abstract String getSymbol();
+
+	protected abstract String getName();
+
+	@Before
+	public void beforeTest() throws DataException {
+		Common.useDefaultSettings();
+	}
+
+	@Test
+	public void testCompile() {
+		PrivateKeyAccount tradeAccount = createTradeAccount(null);
+
+		byte[] creationBytes = buildQortalAT(tradeAccount.getAddress(), getPublicKey(), redeemAmount, foreignAmount, tradeTimeout);
+		assertNotNull(creationBytes);
+
+		System.out.println("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 tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+
+			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 = partnersInitialBalance;
+			actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner'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 = partnersInitialBalance;
+			actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner'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 tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+			// Send creator's address to AT, instead of typical partner's address
+			byte[] messageData = getInstance().buildCancelMessage(deployer.getAddress());
+			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
+			long messageFee = messageTransaction.getTransactionData().getFee();
+
+			// AT should process 'cancel' message in next block
+			BlockUtils.mintBlock(repository);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertTrue(atData.getIsFinished());
+
+			// AT should be in CANCELLED mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.CANCELLED, tradeData.mode);
+
+			// Check balances
+			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);
+
+			// Test orphaning
+			BlockUtils.orphanLastBlock(repository);
+
+			// Check balances
+			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 testOfferCancelInvalidLength() throws DataException {
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+			// Instead of sending creator's address to AT, send too-short/invalid message
+			byte[] messageData = new byte[7];
+			RANDOM.nextBytes(messageData);
+			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
+			long messageFee = messageTransaction.getTransactionData().getFee();
+
+			// AT should process 'cancel' message in next block
+			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
+			BlockUtils.mintBlock(repository);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertTrue(atData.getIsFinished());
+
+			// AT should be in CANCELLED mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.CANCELLED, tradeData.mode);
+		}
+	}
+
+	@SuppressWarnings("unused")
+	@Test
+	public void testTradingInfoProcessing() throws DataException {
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
+			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
+
+			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+			describeAt(repository, atAddress);
+
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+
+			// AT should be in TRADE mode
+			assertEquals(AcctMode.TRADING, tradeData.mode);
+
+			// Check hashOfSecretA was extracted correctly
+			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
+
+			// Check trade partner Qortal address was extracted correctly
+			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
+
+			// Check trade partner's Foreign Coin PKH was extracted correctly
+			assertTrue(Arrays.equals(getPublicKey(), tradeData.partnerForeignPKH));
+
+			// Test orphaning
+			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
+
+			// Check balances
+			long expectedBalance = deployersPostDeploymentBalance;
+			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+		}
+	}
+
+	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT BUT NOT FROM AT CREATOR
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
+
+			BlockUtils.mintBlock(repository);
+
+			long expectedBalance = partnersInitialBalance;
+			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
+
+			describeAt(repository, atAddress);
+
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+
+			// AT should still be in OFFER mode
+			assertEquals(AcctMode.OFFERING, tradeData.mode);
+		}
+	}
+
+	@SuppressWarnings("unused")
+	@Test
+	public void testAutomaticTradeRefund() throws DataException {
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
+			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
+
+			// Check refund
+			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertTrue(atData.getIsFinished());
+
+			// AT should be in REFUNDED mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.REFUNDED, tradeData.mode);
+
+			// Test orphaning
+			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
+
+			// Check balances
+			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 tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+			// Give AT time to process message
+			BlockUtils.mintBlock(repository);
+
+			// Send correct secret to AT, from correct account
+			messageData = buildRedeemMessage(secretA,  partner.getAddress());
+			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+			// AT should send funds in the next block
+			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+			BlockUtils.mintBlock(repository);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertTrue(atData.getIsFinished());
+
+			// AT should be in REDEEMED mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.REDEEMED, tradeData.mode);
+
+			// Check balances
+			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
+			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
+
+			// Orphan redeem
+			BlockUtils.orphanLastBlock(repository);
+
+			// Check balances
+			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
+			actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner'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 tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+			// Give AT time to process message
+			BlockUtils.mintBlock(repository);
+
+			// Send correct secret to AT, but from wrong account
+			messageData = buildRedeemMessage(secretA, partner.getAddress());
+			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
+
+			// AT should NOT send funds in the next block
+			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+			BlockUtils.mintBlock(repository);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is NOT finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertFalse(atData.getIsFinished());
+
+			// AT should still be in TRADE mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.TRADING, tradeData.mode);
+
+			// Check balances
+			long expectedBalance = partnersInitialBalance;
+			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
+
+			// Check eventual refund
+			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 tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+			// Give AT time to process message
+			BlockUtils.mintBlock(repository);
+
+			// Send incorrect secret to AT, from correct account
+			byte[] wrongSecret = new byte[32];
+			RANDOM.nextBytes(wrongSecret);
+			messageData = buildRedeemMessage(wrongSecret, partner.getAddress());
+			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+			// AT should NOT send funds in the next block
+			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+			BlockUtils.mintBlock(repository);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is NOT finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertFalse(atData.getIsFinished());
+
+			// AT should still be in TRADE mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.TRADING, tradeData.mode);
+
+			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
+			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
+
+			// Check eventual refund
+			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+		}
+	}
+
+	@SuppressWarnings("unused")
+	@Test
+	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+			Account at = deployAtTransaction.getATAccount();
+			String atAddress = at.getAddress();
+
+			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+			int refundTimeout = calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+			// Send trade info to AT
+			byte[] messageData = buildTradeMessage(partner.getAddress(), getPublicKey(), hashOfSecretA, lockTimeA, refundTimeout);
+			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+			// Give AT time to process message
+			BlockUtils.mintBlock(repository);
+
+			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
+			messageData = Bytes.concat(secretA);
+			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+			// AT should NOT send funds in the next block
+			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+			BlockUtils.mintBlock(repository);
+
+			describeAt(repository, atAddress);
+
+			// Check AT is NOT finished
+			ATData atData = repository.getATRepository().fromATAddress(atAddress);
+			assertFalse(atData.getIsFinished());
+
+			// AT should be in TRADING mode
+			CrossChainTradeData tradeData = getInstance().populateTradeData(repository, atData);
+			assertEquals(AcctMode.TRADING, tradeData.mode);
+		}
+	}
+
+	@SuppressWarnings("unused")
+	@Test
+	public void testDescribeDeployed() throws DataException {
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+
+			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, getCodeBytesHash()))
+					continue;
+
+				describeAt(repository, atAddress);
+			}
+		}
+	}
+
+	protected int calcTestLockTimeA(long messageTimestamp) {
+		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
+	}
+
+	protected DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
+		byte[] creationBytes = buildQortalAT(tradeAddress, getPublicKey(), redeemAmount, foreignAmount, tradeTimeout);
+
+		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-" + getSymbol() + " cross-chain trade";
+		String description = String.format("Qortal-" + getName() + " cross-chain trade");
+		String atType = "ACCT";
+		String tags = "QORT-" + getSymbol() + " 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;
+	}
+
+	protected 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;
+		int version = 4;
+		int nonce = 0;
+		long amount = 0;
+		Long assetId = null; // because amount is zero
+
+		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
+		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
+
+		MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
+
+		fee = messageTransaction.calcRecommendedFee();
+		messageTransactionData.setFee(fee);
+
+		TransactionUtils.signAndMint(repository, messageTransactionData, sender);
+
+		return messageTransaction;
+	}
+
+	protected void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
+		long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+		int refundTimeout = tradeTimeout / 2 + 1; // close enough
+
+		// 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;
+
+		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);
+	}
+
+	protected void describeAt(Repository repository, String atAddress) throws DataException {
+		ATData atData = repository.getATRepository().fromATAddress(atAddress);
+		CrossChainTradeData tradeData = getInstance().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"
+				+ "\tmode: %s\n"
+				+ "\tcreator: %s,\n"
+				+ "\tcreation timestamp: %s,\n"
+				+ "\tcurrent balance: %s QORT,\n"
+				+ "\tis finished: %b,\n"
+				+ "\tredeem payout: %s QORT,\n"
+				+ "\texpected " + getName() + ": %s " + getSymbol() + ",\n"
+				+ "\tcurrent block height: %d,\n",
+				tradeData.qortalAtAddress,
+				tradeData.mode,
+				tradeData.qortalCreator,
+				epochMilliFormatter.apply(tradeData.creationTimestamp),
+				Amounts.prettyAmount(tradeData.qortBalance),
+				atData.getIsFinished(),
+				Amounts.prettyAmount(tradeData.qortAmount),
+				Amounts.prettyAmount(tradeData.expectedForeignAmount),
+				currentBlockHeight));
+
+		describeRefundAt(tradeData, epochMilliFormatter);
+	}
+
+	protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
+		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
+			System.out.println(String.format("\trefund timeout: %d minutes,\n"
+					+ "\trefund height: block %d,\n"
+					+ "\tHASH160 of secret-A: %s,\n"
+					+ "\t" + getName() + " P2SH-A nLockTime: %d (%s),\n"
+					+ "\ttrade partner: %s\n"
+					+ "\tpartner's receiving address: %s",
+					tradeData.refundTimeout,
+					tradeData.tradeRefundHeight,
+					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
+					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
+					tradeData.qortalPartnerAddress,
+					tradeData.qortalPartnerReceivingAddress));
+		}
+	}
+
+	protected PrivateKeyAccount createTradeAccount(Repository repository) {
+		// We actually use a known test account with funds to avoid PoW compute
+		return Common.getTestAccount(repository, "alice");
+	}
+}
\ No newline at end of file
diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java
index 4487e874..cc33eb43 100644
--- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java
@@ -2,507 +2,89 @@ package org.qortal.test.crosschain.bitcoinv1;
 
 import static org.junit.Assert.*;
 
-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.Random;
 import java.util.function.Function;
 
-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.block.Block;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.BitcoinACCTv1;
 import org.qortal.crosschain.AcctMode;
 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.test.crosschain.ACCTTests;
 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 BitcoinACCTv1Tests extends Common {
+public class BitcoinACCTv1Tests extends ACCTTests {
 
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
 	public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
 	public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long bitcoinAmount = 864200L; // 0.00864200 BTC
 
-	private static final Random RANDOM = new Random();
+	private static final String SYMBOL = "BTC";
 
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	private static final String NAME = "Bitcoin";
+
+	@Override
+	protected byte[] getPublicKey() {
+		return bitcoinPublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return BitcoinACCTv1.buildQortalAT(address,publicKey, hashOfSecretB, redeemAmount, foreignAmount,tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+	@Override
+	protected ACCT getInstance() {
+		return BitcoinACCTv1.getInstance();
+	}
 
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
+	}
 
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return BitcoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
+	}
 
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return BitcoinACCTv1.buildRedeemMessage(secretA,secretB,address);
+	}
 
-			long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return BitcoinACCTv1.CODE_BYTES_HASH;
+	}
 
-			assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
+	}
 
-			expectedBalance = fundingAmount;
-			actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
-
-			assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
-
-			expectedBalance = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
 
 	@SuppressWarnings("unused")
+	@Override
 	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = BitcoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			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 testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's Bitcoin PKH was extracted correctly
-			assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
-	}
-
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			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 testCorrectSecretsCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secrets to AT, from correct account
-			messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 testCorrectSecretsIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secrets to AT, but from wrong account
-			messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testIncorrectSecretsCorrectSender() throws DataException {
+	public void testIncorrectSecretCorrectSender() throws DataException {
 		try (final Repository repository = RepositoryManager.getRepository()) {
 			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
 			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
@@ -582,197 +164,8 @@ public class BitcoinACCTv1Tests extends Common {
 		}
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretsCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secrets to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA, secretB);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, BitcoinACCTv1.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
-
-		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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough
-
-		// 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;
-
-		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 = BitcoinACCTv1.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tHASH160 of secret-B: %s,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected bitcoin: %s BTC,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
+	@Override
+	protected void describeRefundAt(CrossChainTradeData tradeData, Function<Long, String> epochMilliFormatter) {
 		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
 			System.out.println(String.format("\trefund height: block %d,\n"
 					+ "\tHASH160 of secret-A: %s,\n"
@@ -786,10 +179,4 @@ public class BitcoinACCTv1Tests extends Common {
 					tradeData.qortalPartnerAddress));
 		}
 	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv3/BitcoinACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/bitcoinv3/BitcoinACCTv3Tests.java
index 01345727..5e0048bf 100644
--- a/src/test/java/org/qortal/test/crosschain/bitcoinv3/BitcoinACCTv3Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/bitcoinv3/BitcoinACCTv3Tests.java
@@ -1,769 +1,58 @@
 package org.qortal.test.crosschain.bitcoinv3;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-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.block.Block;
-import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.BitcoinACCTv3;
-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 org.qortal.test.crosschain.ACCTTests;
 
-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.Random;
-import java.util.function.Function;
+public class BitcoinACCTv3Tests extends ACCTTests {
 
-import static org.junit.Assert.*;
-
-public class BitcoinACCTv3Tests extends Common {
-
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long bitcoinAmount = 864200L; // 0.00864200 BTC
+	private static final String SYMBOL = "BTC";
+	private static final String NAME = "Bitcoin";
 
-	private static final Random RANDOM = new Random();
-
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return bitcoinPublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return BitcoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return BitcoinACCTv3.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = BitcoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return BitcoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's Bitcoin PKH was extracted correctly
-			assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return BitcoinACCTv3.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return BitcoinACCTv3.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = BitcoinACCTv3.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = BitcoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = BitcoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, BitcoinACCTv3.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, tradeTimeout);
-
-		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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = BitcoinACCTv3.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected Bitcoin: %s BTC,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java
index d13aba4c..01ead678 100644
--- a/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java
@@ -1,769 +1,58 @@
 package org.qortal.test.crosschain.digibytev3;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-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.block.Block;
-import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.DigibyteACCTv3;
-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 org.qortal.test.crosschain.ACCTTests;
 
-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.Random;
-import java.util.function.Function;
+public class DigibyteACCTv3Tests extends ACCTTests {
 
-import static org.junit.Assert.*;
-
-public class DigibyteACCTv3Tests extends Common {
-
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] digibytePublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long digibyteAmount = 864200L; // 0.00864200 DGB
+	private static final String SYMBOL = "DGB";
+	private static final String NAME = "DigiByte";
 
-	private static final Random RANDOM = new Random();
-
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return digibytePublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAccount.getAddress(), digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return DigibyteACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return DigibyteACCTv3.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = DigibyteACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return DigibyteACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's digibyte PKH was extracted correctly
-			assertTrue(Arrays.equals(digibytePublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return DigibyteACCTv3.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return DigibyteACCTv3.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = DigibyteACCTv3.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = DigibyteACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, DigibyteACCTv3.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAddress, digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
-
-		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-DGB cross-chain trade";
-		String description = String.format("Qortal-Digibyte cross-chain trade");
-		String atType = "ACCT";
-		String tags = "QORT-DGB 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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = DigibyteACCTv3.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected digibyte: %s DGB,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tDigibyte P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/dogecoinv3/DogecoinACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/dogecoinv3/DogecoinACCTv3Tests.java
index 7056e433..551173f7 100644
--- a/src/test/java/org/qortal/test/crosschain/dogecoinv3/DogecoinACCTv3Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/dogecoinv3/DogecoinACCTv3Tests.java
@@ -1,769 +1,58 @@
 package org.qortal.test.crosschain.dogecoinv3;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-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.block.Block;
-import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.DogecoinACCTv3;
-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 org.qortal.test.crosschain.ACCTTests;
 
-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.Random;
-import java.util.function.Function;
+public class DogecoinACCTv3Tests extends ACCTTests {
 
-import static org.junit.Assert.*;
-
-public class DogecoinACCTv3Tests extends Common {
-
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] dogecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long dogecoinAmount = 864200L; // 0.00864200 DOGE
+	private static final String SYMBOL = "DOGE";
+	private static final String NAME = "Dogecoin";
 
-	private static final Random RANDOM = new Random();
-
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return dogecoinPublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return DogecoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return DogecoinACCTv3.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = DogecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return DogecoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's dogecoin PKH was extracted correctly
-			assertTrue(Arrays.equals(dogecoinPublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return DogecoinACCTv3.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return DogecoinACCTv3.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = DogecoinACCTv3.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = DogecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = DogecoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = DogecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = DogecoinACCTv3.buildTradeMessage(partner.getAddress(), dogecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = DogecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, DogecoinACCTv3.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = DogecoinACCTv3.buildQortalAT(tradeAddress, dogecoinPublicKeyHash, redeemAmount, dogecoinAmount, tradeTimeout);
-
-		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-DOGE cross-chain trade";
-		String description = String.format("Qortal-Dogecoin cross-chain trade");
-		String atType = "ACCT";
-		String tags = "QORT-DOGE 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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = DogecoinACCTv3.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected dogecoin: %s DOGE,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tDogecoin P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java
index 609ff5f3..91a450d0 100644
--- a/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java
@@ -1,770 +1,60 @@
 package org.qortal.test.crosschain.litecoinv1;
 
-import static org.junit.Assert.*;
-
-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.Random;
-import java.util.function.Function;
-
-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.block.Block;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.LitecoinACCTv1;
-import org.qortal.crosschain.AcctMode;
-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 org.qortal.test.crosschain.ACCTTests;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
 
-public class LitecoinACCTv1Tests extends Common {
+public class LitecoinACCTv1Tests extends ACCTTests {
 
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long litecoinAmount = 864200L; // 0.00864200 LTC
+	private static final String SYMBOL = "LTC";
 
-	private static final Random RANDOM = new Random();
+	private static final String NAME = "Litecoin";
 
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return litecoinPublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return LitecoinACCTv1.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's Litecoin PKH was extracted correctly
-			assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return LitecoinACCTv1.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return LitecoinACCTv1.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = LitecoinACCTv1.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = LitecoinACCTv1.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, LitecoinACCTv1.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
-
-		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-LTC cross-chain trade";
-		String description = String.format("Qortal-Litecoin cross-chain trade");
-		String atType = "ACCT";
-		String tags = "QORT-LTC 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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = LitecoinACCTv1.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected Litecoin: %s LTC,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv3/LitecoinACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/litecoinv3/LitecoinACCTv3Tests.java
index 009af5ea..a1a0bfcc 100644
--- a/src/test/java/org/qortal/test/crosschain/litecoinv3/LitecoinACCTv3Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/litecoinv3/LitecoinACCTv3Tests.java
@@ -1,769 +1,58 @@
 package org.qortal.test.crosschain.litecoinv3;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-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.block.Block;
-import org.qortal.crosschain.AcctMode;
-import org.qortal.crosschain.LitecoinACCTv3;
-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 org.qortal.crosschain.ACCT;
+import org.qortal.crosschain.LitecoinACCTv1;
+import org.qortal.test.crosschain.ACCTTests;
 
-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.Random;
-import java.util.function.Function;
+public class LitecoinACCTv3Tests extends ACCTTests {
 
-import static org.junit.Assert.*;
-
-public class LitecoinACCTv3Tests extends Common {
-
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long litecoinAmount = 864200L; // 0.00864200 LTC
+	private static final String SYMBOL = "LTC";
+	private static final String NAME = "Litecoin";
 
-	private static final Random RANDOM = new Random();
-
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return litecoinPublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return LitecoinACCTv1.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return LitecoinACCTv1.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = LitecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return LitecoinACCTv1.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's Litecoin PKH was extracted correctly
-			assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return LitecoinACCTv1.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return LitecoinACCTv1.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = LitecoinACCTv3.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = LitecoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = LitecoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = LitecoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = LitecoinACCTv3.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, LitecoinACCTv3.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
-
-		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-LTC cross-chain trade";
-		String description = String.format("Qortal-Litecoin cross-chain trade");
-		String atType = "ACCT";
-		String tags = "QORT-LTC 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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = LitecoinACCTv3.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected Litecoin: %s LTC,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/piratechainv3/PirateChainACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/piratechainv3/PirateChainACCTv3Tests.java
index f9ac9de1..18099872 100644
--- a/src/test/java/org/qortal/test/crosschain/piratechainv3/PirateChainACCTv3Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/piratechainv3/PirateChainACCTv3Tests.java
@@ -1,771 +1,58 @@
 package org.qortal.test.crosschain.piratechainv3;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-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.block.Block;
-import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.PirateChainACCTv3;
-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 org.qortal.test.crosschain.ACCTTests;
 
-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.Random;
-import java.util.function.Function;
+public class PirateChainACCTv3Tests extends ACCTTests {
 
-import static org.junit.Assert.*;
-
-public class PirateChainACCTv3Tests extends Common {
-
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] pirateChainPublicKey = HashCode.fromString("aabb00bb11bb22bb33bb44bb55bb66bb77bb88bb99cc00cc11cc22cc33cc44cc55").asBytes(); // 33 bytes
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long arrrAmount = 864200L; // 0.00864200 ARRR
+	private static final String SYMBOL = "ARRR";
+	private static final String NAME = "Pirate Chain";
 
-	private static final Random RANDOM = new Random();
-
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return pirateChainPublicKey;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAccount.getAddress(), pirateChainPublicKey, redeemAmount, arrrAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return PirateChainACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return PirateChainACCTv3.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = PirateChainACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return PirateChainACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			System.out.println(String.format("pirateChainPublicKey: %s", HashCode.fromBytes(pirateChainPublicKey)));
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's Litecoin PKH was extracted correctly
-			assertTrue(Arrays.equals(pirateChainPublicKey, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return PirateChainACCTv3.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return PirateChainACCTv3.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = PirateChainACCTv3.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = PirateChainACCTv3.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = PirateChainACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = PirateChainACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = PirateChainACCTv3.buildTradeMessage(partner.getAddress(), pirateChainPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, PirateChainACCTv3.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeAddress, pirateChainPublicKey, redeemAmount, arrrAmount, tradeTimeout);
-
-		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-ARRR cross-chain trade";
-		String description = String.format("Qortal-PirateChain cross-chain trade");
-		String atType = "ACCT";
-		String tags = "QORT-ARRR 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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = PirateChainACCTv3.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected ARRR: %s ARRR,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tPirate Chain P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }
diff --git a/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java
index 012d5f5d..1af0f7d6 100644
--- a/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java
+++ b/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java
@@ -1,769 +1,58 @@
 package org.qortal.test.crosschain.ravencoinv3;
 
 import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-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.block.Block;
-import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.ACCT;
 import org.qortal.crosschain.RavencoinACCTv3;
-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 org.qortal.test.crosschain.ACCTTests;
 
-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.Random;
-import java.util.function.Function;
+public class RavencoinACCTv3Tests extends ACCTTests {
 
-import static org.junit.Assert.*;
-
-public class RavencoinACCTv3Tests extends Common {
-
-	public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
-	public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
 	public static final byte[] ravencoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
-	public static final int tradeTimeout = 20; // blocks
-	public static final long redeemAmount = 80_40200000L;
-	public static final long fundingAmount = 123_45600000L;
-	public static final long ravencoinAmount = 864200L; // 0.00864200 RVN
+	private static final String SYMBOL = "RVN";
+	private static final String NAME = "Ravencoin";
 
-	private static final Random RANDOM = new Random();
-
-	@Before
-	public void beforeTest() throws DataException {
-		Common.useDefaultSettings();
+	@Override
+	protected byte[] getPublicKey() {
+		return ravencoinPublicKeyHash;
 	}
 
-	@Test
-	public void testCompile() {
-		PrivateKeyAccount tradeAccount = createTradeAccount(null);
-
-		byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAccount.getAddress(), ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
-		assertNotNull(creationBytes);
-
-		System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+	@Override
+	protected byte[] buildQortalAT(String address, byte[] publicKey, long redeemAmount, long foreignAmount, int tradeTimeout) {
+		return RavencoinACCTv3.buildQortalAT(address, publicKey, redeemAmount, foreignAmount, tradeTimeout);
 	}
 
-	@Test
-	public void testDeploy() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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 = partnersInitialBalance;
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected ACCT getInstance() {
+		return RavencoinACCTv3.getInstance();
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancel() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Send creator's address to AT, instead of typical partner's address
-			byte[] messageData = RavencoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-
-			// Check balances
-			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);
-
-			// Test orphaning
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance - messageFee;
-			actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected int calcRefundTimeout(long partnersOfferMessageTransactionTimestamp, int lockTimeA) {
+		return RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testOfferCancelInvalidLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			// Instead of sending creator's address to AT, send too-short/invalid message
-			byte[] messageData = new byte[7];
-			RANDOM.nextBytes(messageData);
-			MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
-			long messageFee = messageTransaction.getTransactionData().getFee();
-
-			// AT should process 'cancel' message in next block
-			// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in CANCELLED mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.CANCELLED, tradeData.mode);
-		}
+	@Override
+	protected byte[] buildTradeMessage(String address, byte[] publicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+		return RavencoinACCTv3.buildTradeMessage(address, publicKey, hashOfSecretA, lockTimeA, refundTimeout);
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testTradingInfoProcessing() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should be in TRADE mode
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check hashOfSecretA was extracted correctly
-			assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
-
-			// Check trade partner Qortal address was extracted correctly
-			assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
-
-			// Check trade partner's ravencoin PKH was extracted correctly
-			assertTrue(Arrays.equals(ravencoinPublicKeyHash, tradeData.partnerForeignPKH));
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected byte[] buildRedeemMessage(byte[] secretA, String address) {
+		return RavencoinACCTv3.buildRedeemMessage(secretA, address);
 	}
 
-	// TEST SENDING TRADING INFO 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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT BUT NOT FROM AT CREATOR
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			BlockUtils.mintBlock(repository);
-
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
-
-			describeAt(repository, atAddress);
-
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-
-			// AT should still be in OFFER mode
-			assertEquals(AcctMode.OFFERING, tradeData.mode);
-		}
+	@Override
+	protected byte[] getCodeBytesHash() {
+		return RavencoinACCTv3.CODE_BYTES_HASH;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testAutomaticTradeRefund() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			Block postDeploymentBlock = BlockUtils.mintBlock(repository);
-			int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
-
-			// Check refund
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-			long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
-
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REFUNDED mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REFUNDED, tradeData.mode);
-
-			// Test orphaning
-			BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
-
-			// Check balances
-			long expectedBalance = deployersPostDeploymentBalance;
-			long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
-		}
+	@Override
+	protected String getSymbol() {
+		return SYMBOL;
 	}
 
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account
-			messageData = RavencoinACCTv3.buildRedeemMessage(secretA,  partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertTrue(atData.getIsFinished());
-
-			// AT should be in REDEEMED mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.REDEEMED, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
-
-			// Orphan redeem
-			BlockUtils.orphanLastBlock(repository);
-
-			// Check balances
-			expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner'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()));
-		}
+	@Override
+	protected String getName() {
+		return NAME;
 	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretIncorrectSender() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, but from wrong account
-			messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
-			messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			// Check balances
-			long expectedBalance = partnersInitialBalance;
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			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 tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			long deployAtFee = deployAtTransaction.getTransactionData().getFee();
-
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send incorrect secret to AT, from correct account
-			byte[] wrongSecret = new byte[32];
-			RANDOM.nextBytes(wrongSecret);
-			messageData = RavencoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should still be in TRADE mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-
-			long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
-			long actualBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
-
-			// Check eventual refund
-			checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-			Account at = deployAtTransaction.getATAccount();
-			String atAddress = at.getAddress();
-
-			long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
-			int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
-			int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
-
-			// Send trade info to AT
-			byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
-			MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
-
-			// Give AT time to process message
-			BlockUtils.mintBlock(repository);
-
-			// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
-			messageData = Bytes.concat(secretA);
-			messageTransaction = sendMessage(repository, partner, messageData, atAddress);
-
-			// AT should NOT send funds in the next block
-			ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
-			BlockUtils.mintBlock(repository);
-
-			describeAt(repository, atAddress);
-
-			// Check AT is NOT finished
-			ATData atData = repository.getATRepository().fromATAddress(atAddress);
-			assertFalse(atData.getIsFinished());
-
-			// AT should be in TRADING mode
-			CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
-			assertEquals(AcctMode.TRADING, tradeData.mode);
-		}
-	}
-
-	@SuppressWarnings("unused")
-	@Test
-	public void testDescribeDeployed() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
-			PrivateKeyAccount tradeAccount = createTradeAccount(repository);
-
-			PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
-
-			long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
-			long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
-
-			DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
-
-			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, RavencoinACCTv3.CODE_BYTES_HASH))
-					continue;
-
-				describeAt(repository, atAddress);
-			}
-		}
-	}
-
-	private int calcTestLockTimeA(long messageTimestamp) {
-		return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
-	}
-
-	private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
-		byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAddress, ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
-
-		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-RVN cross-chain trade";
-		String description = String.format("Qortal-Ravencoin cross-chain trade");
-		String atType = "ACCT";
-		String tags = "QORT-RVN 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;
-		int version = 4;
-		int nonce = 0;
-		long amount = 0;
-		Long assetId = null; // because amount is zero
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
-		TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, 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;
-		int refundTimeout = tradeTimeout / 2 + 1; // close enough
-
-		// 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;
-
-		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 = RavencoinACCTv3.getInstance().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"
-				+ "\tmode: %s\n"
-				+ "\tcreator: %s,\n"
-				+ "\tcreation timestamp: %s,\n"
-				+ "\tcurrent balance: %s QORT,\n"
-				+ "\tis finished: %b,\n"
-				+ "\tredeem payout: %s QORT,\n"
-				+ "\texpected ravencoin: %s RVN,\n"
-				+ "\tcurrent block height: %d,\n",
-				tradeData.qortalAtAddress,
-				tradeData.mode,
-				tradeData.qortalCreator,
-				epochMilliFormatter.apply(tradeData.creationTimestamp),
-				Amounts.prettyAmount(tradeData.qortBalance),
-				atData.getIsFinished(),
-				Amounts.prettyAmount(tradeData.qortAmount),
-				Amounts.prettyAmount(tradeData.expectedForeignAmount),
-				currentBlockHeight));
-
-		if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
-			System.out.println(String.format("\trefund timeout: %d minutes,\n"
-					+ "\trefund height: block %d,\n"
-					+ "\tHASH160 of secret-A: %s,\n"
-					+ "\tRavencoin P2SH-A nLockTime: %d (%s),\n"
-					+ "\ttrade partner: %s\n"
-					+ "\tpartner's receiving address: %s",
-					tradeData.refundTimeout,
-					tradeData.tradeRefundHeight,
-					HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
-					tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
-					tradeData.qortalPartnerAddress,
-					tradeData.qortalPartnerReceivingAddress));
-		}
-	}
-
-	private PrivateKeyAccount createTradeAccount(Repository repository) {
-		// We actually use a known test account with funds to avoid PoW compute
-		return Common.getTestAccount(repository, "alice");
-	}
-
 }