forked from Qortal-Forker/qortal
		
	Merge branch 'digibyte' into new-coins
This commit is contained in:
		
							
								
								
									
										4
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								pom.xml
									
									
									
									
									
								
							@@ -7,7 +7,7 @@
 | 
			
		||||
	<packaging>jar</packaging>
 | 
			
		||||
	<properties>
 | 
			
		||||
		<skipTests>true</skipTests>
 | 
			
		||||
		<altcoinj.version>bf9fb80</altcoinj.version>
 | 
			
		||||
		<altcoinj.version>02e0d3d</altcoinj.version>
 | 
			
		||||
		<bitcoinj.version>0.15.10</bitcoinj.version>
 | 
			
		||||
		<bouncycastle.version>1.64</bouncycastle.version>
 | 
			
		||||
		<build.timestamp>${maven.build.timestamp}</build.timestamp>
 | 
			
		||||
@@ -444,7 +444,7 @@
 | 
			
		||||
		</dependency>
 | 
			
		||||
		<!-- For Litecoin, etc. support, requires bitcoinj -->
 | 
			
		||||
		<dependency>
 | 
			
		||||
			<groupId>com.github.jjos2372</groupId>
 | 
			
		||||
			<groupId>com.github.qortal</groupId>
 | 
			
		||||
			<artifactId>altcoinj</artifactId>
 | 
			
		||||
			<version>${altcoinj.version}</version>
 | 
			
		||||
		</dependency>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package org.qortal.api.model.crosschain;
 | 
			
		||||
 | 
			
		||||
import javax.xml.bind.annotation.XmlAccessType;
 | 
			
		||||
import javax.xml.bind.annotation.XmlAccessorType;
 | 
			
		||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 | 
			
		||||
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
 | 
			
		||||
@XmlAccessorType(XmlAccessType.FIELD)
 | 
			
		||||
public class DigibyteSendRequest {
 | 
			
		||||
 | 
			
		||||
	@Schema(description = "Digibyte BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
 | 
			
		||||
	public String xprv58;
 | 
			
		||||
 | 
			
		||||
	@Schema(description = "Recipient's Digibyte address ('legacy' P2PKH only)", example = "1DigByteEaterAddressDontSendf59kuE")
 | 
			
		||||
	public String receivingAddress;
 | 
			
		||||
 | 
			
		||||
	@Schema(description = "Amount of DGB to send", type = "number")
 | 
			
		||||
	@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
 | 
			
		||||
	public long digibyteAmount;
 | 
			
		||||
 | 
			
		||||
	@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DGB (100 sats) per byte", example = "0.00000100", type = "number")
 | 
			
		||||
	@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
 | 
			
		||||
	public Long feePerByte;
 | 
			
		||||
 | 
			
		||||
	public DigibyteSendRequest() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,177 @@
 | 
			
		||||
package org.qortal.api.resource;
 | 
			
		||||
 | 
			
		||||
import io.swagger.v3.oas.annotations.Operation;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Content;
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
 | 
			
		||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
 | 
			
		||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 | 
			
		||||
import io.swagger.v3.oas.annotations.tags.Tag;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import javax.ws.rs.HeaderParam;
 | 
			
		||||
import javax.ws.rs.POST;
 | 
			
		||||
import javax.ws.rs.Path;
 | 
			
		||||
import javax.ws.rs.core.Context;
 | 
			
		||||
import javax.ws.rs.core.MediaType;
 | 
			
		||||
 | 
			
		||||
import org.bitcoinj.core.Transaction;
 | 
			
		||||
import org.qortal.api.ApiError;
 | 
			
		||||
import org.qortal.api.ApiErrors;
 | 
			
		||||
import org.qortal.api.ApiExceptionFactory;
 | 
			
		||||
import org.qortal.api.Security;
 | 
			
		||||
import org.qortal.api.model.crosschain.DigibyteSendRequest;
 | 
			
		||||
import org.qortal.crosschain.Digibyte;
 | 
			
		||||
import org.qortal.crosschain.ForeignBlockchainException;
 | 
			
		||||
import org.qortal.crosschain.SimpleTransaction;
 | 
			
		||||
 | 
			
		||||
@Path("/crosschain/dgb")
 | 
			
		||||
@Tag(name = "Cross-Chain (Digibyte)")
 | 
			
		||||
public class CrossChainDigibyteResource {
 | 
			
		||||
 | 
			
		||||
	@Context
 | 
			
		||||
	HttpServletRequest request;
 | 
			
		||||
 | 
			
		||||
	@POST
 | 
			
		||||
	@Path("/walletbalance")
 | 
			
		||||
	@Operation(
 | 
			
		||||
		summary = "Returns DGB balance for hierarchical, deterministic BIP32 wallet",
 | 
			
		||||
		description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
 | 
			
		||||
		requestBody = @RequestBody(
 | 
			
		||||
			required = true,
 | 
			
		||||
			content = @Content(
 | 
			
		||||
				mediaType = MediaType.TEXT_PLAIN,
 | 
			
		||||
				schema = @Schema(
 | 
			
		||||
					type = "string",
 | 
			
		||||
					description = "BIP32 'm' private/public key in base58",
 | 
			
		||||
					example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
 | 
			
		||||
				)
 | 
			
		||||
			)
 | 
			
		||||
		),
 | 
			
		||||
		responses = {
 | 
			
		||||
			@ApiResponse(
 | 
			
		||||
				content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
	@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
 | 
			
		||||
	@SecurityRequirement(name = "apiKey")
 | 
			
		||||
	public String getDigibyteWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
 | 
			
		||||
		Security.checkApiCallAllowed(request);
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
 | 
			
		||||
		if (!digibyte.isValidDeterministicKey(key58))
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			Long balance = digibyte.getWalletBalanceFromTransactions(key58);
 | 
			
		||||
			if (balance == null)
 | 
			
		||||
				throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
 | 
			
		||||
 | 
			
		||||
			return balance.toString();
 | 
			
		||||
 | 
			
		||||
		} catch (ForeignBlockchainException e) {
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@POST
 | 
			
		||||
	@Path("/wallettransactions")
 | 
			
		||||
	@Operation(
 | 
			
		||||
		summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
 | 
			
		||||
		description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
 | 
			
		||||
		requestBody = @RequestBody(
 | 
			
		||||
			required = true,
 | 
			
		||||
			content = @Content(
 | 
			
		||||
				mediaType = MediaType.TEXT_PLAIN,
 | 
			
		||||
				schema = @Schema(
 | 
			
		||||
					type = "string",
 | 
			
		||||
					description = "BIP32 'm' private/public key in base58",
 | 
			
		||||
					example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
 | 
			
		||||
				)
 | 
			
		||||
			)
 | 
			
		||||
		),
 | 
			
		||||
		responses = {
 | 
			
		||||
			@ApiResponse(
 | 
			
		||||
				content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
	@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
 | 
			
		||||
	@SecurityRequirement(name = "apiKey")
 | 
			
		||||
	public List<SimpleTransaction> getDigibyteWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
 | 
			
		||||
		Security.checkApiCallAllowed(request);
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
 | 
			
		||||
		if (!digibyte.isValidDeterministicKey(key58))
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			return digibyte.getWalletTransactions(key58);
 | 
			
		||||
		} catch (ForeignBlockchainException e) {
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@POST
 | 
			
		||||
	@Path("/send")
 | 
			
		||||
	@Operation(
 | 
			
		||||
		summary = "Sends DGB from hierarchical, deterministic BIP32 wallet to specific address",
 | 
			
		||||
		description = "Currently supports 'legacy' P2PKH Digibyte addresses and Native SegWit (P2WPKH) addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
 | 
			
		||||
		requestBody = @RequestBody(
 | 
			
		||||
			required = true,
 | 
			
		||||
			content = @Content(
 | 
			
		||||
				mediaType = MediaType.APPLICATION_JSON,
 | 
			
		||||
				schema = @Schema(
 | 
			
		||||
					implementation = DigibyteSendRequest.class
 | 
			
		||||
				)
 | 
			
		||||
			)
 | 
			
		||||
		),
 | 
			
		||||
		responses = {
 | 
			
		||||
			@ApiResponse(
 | 
			
		||||
				content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
	@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
 | 
			
		||||
	@SecurityRequirement(name = "apiKey")
 | 
			
		||||
	public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, DigibyteSendRequest digibyteSendRequest) {
 | 
			
		||||
		Security.checkApiCallAllowed(request);
 | 
			
		||||
 | 
			
		||||
		if (digibyteSendRequest.digibyteAmount <= 0)
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
 | 
			
		||||
 | 
			
		||||
		if (digibyteSendRequest.feePerByte != null && digibyteSendRequest.feePerByte <= 0)
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
 | 
			
		||||
		if (!digibyte.isValidAddress(digibyteSendRequest.receivingAddress))
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
 | 
			
		||||
 | 
			
		||||
		if (!digibyte.isValidDeterministicKey(digibyteSendRequest.xprv58))
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
 | 
			
		||||
 | 
			
		||||
		Transaction spendTransaction = digibyte.buildSpend(digibyteSendRequest.xprv58,
 | 
			
		||||
				digibyteSendRequest.receivingAddress,
 | 
			
		||||
				digibyteSendRequest.digibyteAmount,
 | 
			
		||||
				digibyteSendRequest.feePerByte);
 | 
			
		||||
 | 
			
		||||
		if (spendTransaction == null)
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			digibyte.broadcastTransaction(spendTransaction);
 | 
			
		||||
		} catch (ForeignBlockchainException e) {
 | 
			
		||||
			throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return spendTransaction.getTxId().toString();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,885 @@
 | 
			
		||||
package org.qortal.controller.tradebot;
 | 
			
		||||
 | 
			
		||||
import org.apache.logging.log4j.LogManager;
 | 
			
		||||
import org.apache.logging.log4j.Logger;
 | 
			
		||||
import org.bitcoinj.core.*;
 | 
			
		||||
import org.bitcoinj.script.Script.ScriptType;
 | 
			
		||||
import org.qortal.account.PrivateKeyAccount;
 | 
			
		||||
import org.qortal.account.PublicKeyAccount;
 | 
			
		||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
 | 
			
		||||
import org.qortal.asset.Asset;
 | 
			
		||||
import org.qortal.crosschain.*;
 | 
			
		||||
import org.qortal.crypto.Crypto;
 | 
			
		||||
import org.qortal.data.at.ATData;
 | 
			
		||||
import org.qortal.data.crosschain.CrossChainTradeData;
 | 
			
		||||
import org.qortal.data.crosschain.TradeBotData;
 | 
			
		||||
import org.qortal.data.transaction.BaseTransactionData;
 | 
			
		||||
import org.qortal.data.transaction.DeployAtTransactionData;
 | 
			
		||||
import org.qortal.data.transaction.MessageTransactionData;
 | 
			
		||||
import org.qortal.group.Group;
 | 
			
		||||
import org.qortal.repository.DataException;
 | 
			
		||||
import org.qortal.repository.Repository;
 | 
			
		||||
import org.qortal.transaction.DeployAtTransaction;
 | 
			
		||||
import org.qortal.transaction.MessageTransaction;
 | 
			
		||||
import org.qortal.transaction.Transaction.ValidationResult;
 | 
			
		||||
import org.qortal.transform.TransformationException;
 | 
			
		||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
 | 
			
		||||
import org.qortal.utils.Base58;
 | 
			
		||||
import org.qortal.utils.NTP;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static java.util.Arrays.stream;
 | 
			
		||||
import static java.util.stream.Collectors.toMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Performing cross-chain trading steps on behalf of user.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * We deal with three different independent state-spaces here:
 | 
			
		||||
 * <ul>
 | 
			
		||||
 * 	<li>Qortal blockchain</li>
 | 
			
		||||
 * 	<li>Foreign blockchain</li>
 | 
			
		||||
 * 	<li>Trade-bot entries</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 */
 | 
			
		||||
public class DigibyteACCTv3TradeBot implements AcctTradeBot {
 | 
			
		||||
 | 
			
		||||
	private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3TradeBot.class);
 | 
			
		||||
 | 
			
		||||
	public enum State implements TradeBot.StateNameAndValueSupplier {
 | 
			
		||||
		BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
 | 
			
		||||
		BOB_WAITING_FOR_MESSAGE(15, true, true),
 | 
			
		||||
		BOB_WAITING_FOR_AT_REDEEM(25, true, true),
 | 
			
		||||
		BOB_DONE(30, false, false),
 | 
			
		||||
		BOB_REFUNDED(35, false, false),
 | 
			
		||||
 | 
			
		||||
		ALICE_WAITING_FOR_AT_LOCK(85, true, true),
 | 
			
		||||
		ALICE_DONE(95, false, false),
 | 
			
		||||
		ALICE_REFUNDING_A(105, true, true),
 | 
			
		||||
		ALICE_REFUNDED(110, false, false);
 | 
			
		||||
 | 
			
		||||
		private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
 | 
			
		||||
 | 
			
		||||
		public final int value;
 | 
			
		||||
		public final boolean requiresAtData;
 | 
			
		||||
		public final boolean requiresTradeData;
 | 
			
		||||
 | 
			
		||||
		State(int value, boolean requiresAtData, boolean requiresTradeData) {
 | 
			
		||||
			this.value = value;
 | 
			
		||||
			this.requiresAtData = requiresAtData;
 | 
			
		||||
			this.requiresTradeData = requiresTradeData;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public static State valueOf(int value) {
 | 
			
		||||
			return map.get(value);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public String getState() {
 | 
			
		||||
			return this.name();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int getStateValue() {
 | 
			
		||||
			return this.value;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
 | 
			
		||||
	private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
 | 
			
		||||
 | 
			
		||||
	private static DigibyteACCTv3TradeBot instance;
 | 
			
		||||
 | 
			
		||||
	private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
 | 
			
		||||
			.map(State::name)
 | 
			
		||||
			.collect(Collectors.toUnmodifiableList());
 | 
			
		||||
 | 
			
		||||
	private DigibyteACCTv3TradeBot() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static synchronized DigibyteACCTv3TradeBot getInstance() {
 | 
			
		||||
		if (instance == null)
 | 
			
		||||
			instance = new DigibyteACCTv3TradeBot();
 | 
			
		||||
 | 
			
		||||
		return instance;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public List<String> getEndStates() {
 | 
			
		||||
		return this.endStates;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DGB.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Generates:
 | 
			
		||||
	 * <ul>
 | 
			
		||||
	 * 	<li>new 'trade' private key</li>
 | 
			
		||||
	 * </ul>
 | 
			
		||||
	 * Derives:
 | 
			
		||||
	 * <ul>
 | 
			
		||||
	 * 	<li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
 | 
			
		||||
	 * 	<li>'foreign' (as in Digibyte) public key, public key hash</li>
 | 
			
		||||
	 * </ul>
 | 
			
		||||
	 * A Qortal AT is then constructed including the following as constants in the 'data segment':
 | 
			
		||||
	 * <ul>
 | 
			
		||||
	 * 	<li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
 | 
			
		||||
	 * 	<li>'foreign'/Digibyte public key hash - used by Alice's P2SH scripts to allow redeem</li>
 | 
			
		||||
	 * 	<li>QORT amount on offer by Bob</li>
 | 
			
		||||
	 * 	<li>DGB amount expected in return by Bob (from Alice)</li>
 | 
			
		||||
	 * 	<li>trading timeout, in case things go wrong and everyone needs to refund</li>
 | 
			
		||||
	 * </ul>
 | 
			
		||||
	 * Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Trade-bot will wait for Bob's AT to be deployed before taking next step.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * @param repository
 | 
			
		||||
	 * @param tradeBotCreateRequest
 | 
			
		||||
	 * @return raw, unsigned DEPLOY_AT transaction
 | 
			
		||||
	 * @throws DataException
 | 
			
		||||
	 */
 | 
			
		||||
	public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
 | 
			
		||||
		byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
 | 
			
		||||
 | 
			
		||||
		byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
 | 
			
		||||
		byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
 | 
			
		||||
		String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
 | 
			
		||||
 | 
			
		||||
		byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
 | 
			
		||||
		byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
 | 
			
		||||
 | 
			
		||||
		// Convert Digibyte receiving address into public key hash (we only support P2PKH at this time)
 | 
			
		||||
		Address digibyteReceivingAddress;
 | 
			
		||||
		try {
 | 
			
		||||
			digibyteReceivingAddress = Address.fromString(Digibyte.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
 | 
			
		||||
		} catch (AddressFormatException e) {
 | 
			
		||||
			throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
 | 
			
		||||
		}
 | 
			
		||||
		if (digibyteReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
 | 
			
		||||
			throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
 | 
			
		||||
 | 
			
		||||
		byte[] digibyteReceivingAccountInfo = digibyteReceivingAddress.getHash();
 | 
			
		||||
 | 
			
		||||
		PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
 | 
			
		||||
 | 
			
		||||
		// Deploy AT
 | 
			
		||||
		long timestamp = NTP.getTime();
 | 
			
		||||
		byte[] reference = creator.getLastReference();
 | 
			
		||||
		long fee = 0L;
 | 
			
		||||
		byte[] signature = null;
 | 
			
		||||
		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
 | 
			
		||||
 | 
			
		||||
		String name = "QORT/DGB ACCT";
 | 
			
		||||
		String description = "QORT/DGB cross-chain trade";
 | 
			
		||||
		String aTType = "ACCT";
 | 
			
		||||
		String tags = "ACCT QORT DGB";
 | 
			
		||||
		byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
 | 
			
		||||
				tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
 | 
			
		||||
		long amount = tradeBotCreateRequest.fundingQortAmount;
 | 
			
		||||
 | 
			
		||||
		DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
 | 
			
		||||
 | 
			
		||||
		DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
 | 
			
		||||
		fee = deployAtTransaction.calcRecommendedFee();
 | 
			
		||||
		deployAtTransactionData.setFee(fee);
 | 
			
		||||
 | 
			
		||||
		DeployAtTransaction.ensureATAddress(deployAtTransactionData);
 | 
			
		||||
		String atAddress = deployAtTransactionData.getAtAddress();
 | 
			
		||||
 | 
			
		||||
		TradeBotData tradeBotData =  new TradeBotData(tradePrivateKey, DigibyteACCTv3.NAME,
 | 
			
		||||
				State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
 | 
			
		||||
				creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
 | 
			
		||||
				tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
 | 
			
		||||
				null, null,
 | 
			
		||||
				SupportedBlockchain.DIGIBYTE.name(),
 | 
			
		||||
				tradeForeignPublicKey, tradeForeignPublicKeyHash,
 | 
			
		||||
				tradeBotCreateRequest.foreignAmount, null, null, null, digibyteReceivingAccountInfo);
 | 
			
		||||
 | 
			
		||||
		TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
 | 
			
		||||
 | 
			
		||||
		// Attempt to backup the trade bot data
 | 
			
		||||
		TradeBot.backupTradeBotData(repository, null);
 | 
			
		||||
 | 
			
		||||
		// Return to user for signing and broadcast as we don't have their Qortal private key
 | 
			
		||||
		try {
 | 
			
		||||
			return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
 | 
			
		||||
		} catch (TransformationException e) {
 | 
			
		||||
			throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DGB to an existing offer.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
 | 
			
		||||
	 * and access to a Digibyte wallet via <tt>xprv58</tt>.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The <tt>crossChainTradeData</tt> contains the current trade offer state
 | 
			
		||||
	 * as extracted from the AT's data segment.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Access to a funded wallet is via a Digibyte BIP32 hierarchical deterministic key,
 | 
			
		||||
	 * passed via <tt>xprv58</tt>.
 | 
			
		||||
	 * <b>This key will be stored in your node's database</b>
 | 
			
		||||
	 * to allow trade-bot to create/fund the necessary P2SH transactions!
 | 
			
		||||
	 * However, due to the nature of BIP32 keys, it is possible to give the trade-bot
 | 
			
		||||
	 * only a subset of wallet access (see BIP32 for more details).
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
 | 
			
		||||
	 * Electrum wallet by going to the console tab and entering:<br>
 | 
			
		||||
	 * <tt>wallet.keystore.xprv</tt><br>
 | 
			
		||||
	 * which should result in a base58 string starting with either 'xprv' (for Digibyte main-net)
 | 
			
		||||
	 * or 'tprv' for (Digibyte test-net).
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
 | 
			
		||||
	 * with the Digibyte amount expected by 'Bob'.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * If the Digibyte transaction is successfully broadcast to the network then
 | 
			
		||||
	 * we also send a MESSAGE to Bob's trade-bot to let them know.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * The trade-bot entry is saved to the repository and the cross-chain trading process commences.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * @param repository
 | 
			
		||||
	 * @param crossChainTradeData chosen trade OFFER that Alice wants to match
 | 
			
		||||
	 * @param xprv58 funded wallet xprv in base58
 | 
			
		||||
	 * @return true if P2SH-A funding transaction successfully broadcast to Digibyte network, false otherwise
 | 
			
		||||
	 * @throws DataException
 | 
			
		||||
	 */
 | 
			
		||||
	public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
 | 
			
		||||
		byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
 | 
			
		||||
		byte[] secretA = TradeBot.generateSecret();
 | 
			
		||||
		byte[] hashOfSecretA = Crypto.hash160(secretA);
 | 
			
		||||
 | 
			
		||||
		byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
 | 
			
		||||
		byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
 | 
			
		||||
		String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
 | 
			
		||||
 | 
			
		||||
		byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
 | 
			
		||||
		byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
 | 
			
		||||
		byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
 | 
			
		||||
 | 
			
		||||
		// We need to generate lockTime-A: add tradeTimeout to now
 | 
			
		||||
		long now = NTP.getTime();
 | 
			
		||||
		int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
 | 
			
		||||
 | 
			
		||||
		TradeBotData tradeBotData =  new TradeBotData(tradePrivateKey, DigibyteACCTv3.NAME,
 | 
			
		||||
				State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
 | 
			
		||||
				receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
 | 
			
		||||
				tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
 | 
			
		||||
				secretA, hashOfSecretA,
 | 
			
		||||
				SupportedBlockchain.DIGIBYTE.name(),
 | 
			
		||||
				tradeForeignPublicKey, tradeForeignPublicKeyHash,
 | 
			
		||||
				crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
 | 
			
		||||
 | 
			
		||||
		// Attempt to backup the trade bot data
 | 
			
		||||
		// Include tradeBotData as an additional parameter, since it's not in the repository yet
 | 
			
		||||
		TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
 | 
			
		||||
 | 
			
		||||
		// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
 | 
			
		||||
		long p2shFee;
 | 
			
		||||
		try {
 | 
			
		||||
			p2shFee = Digibyte.getInstance().getP2shFee(now);
 | 
			
		||||
		} catch (ForeignBlockchainException e) {
 | 
			
		||||
			LOGGER.debug("Couldn't estimate Digibyte fees?");
 | 
			
		||||
			return ResponseResult.NETWORK_ISSUE;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Fee for redeem/refund is subtracted from P2SH-A balance.
 | 
			
		||||
		// Do not include fee for funding transaction as this is covered by buildSpend()
 | 
			
		||||
		long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
 | 
			
		||||
 | 
			
		||||
		// P2SH-A to be funded
 | 
			
		||||
		byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
 | 
			
		||||
		String p2shAddress = Digibyte.getInstance().deriveP2shAddress(redeemScriptBytes);
 | 
			
		||||
 | 
			
		||||
		// Build transaction for funding P2SH-A
 | 
			
		||||
		Transaction p2shFundingTransaction = Digibyte.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
 | 
			
		||||
		if (p2shFundingTransaction == null) {
 | 
			
		||||
			LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
 | 
			
		||||
			return ResponseResult.BALANCE_ISSUE;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			Digibyte.getInstance().broadcastTransaction(p2shFundingTransaction);
 | 
			
		||||
		} catch (ForeignBlockchainException e) {
 | 
			
		||||
			// We couldn't fund P2SH-A at this time
 | 
			
		||||
			LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
 | 
			
		||||
			return ResponseResult.NETWORK_ISSUE;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Attempt to send MESSAGE to Bob's Qortal trade address
 | 
			
		||||
		byte[] messageData = DigibyteACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
 | 
			
		||||
		String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
 | 
			
		||||
 | 
			
		||||
		boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
 | 
			
		||||
		if (!isMessageAlreadySent) {
 | 
			
		||||
			PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
 | 
			
		||||
			MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
 | 
			
		||||
 | 
			
		||||
			messageTransaction.computeNonce();
 | 
			
		||||
			messageTransaction.sign(sender);
 | 
			
		||||
 | 
			
		||||
			// reset repository state to prevent deadlock
 | 
			
		||||
			repository.discardChanges();
 | 
			
		||||
			ValidationResult result = messageTransaction.importAsUnconfirmed();
 | 
			
		||||
 | 
			
		||||
			if (result != ValidationResult.OK) {
 | 
			
		||||
				LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
 | 
			
		||||
				return ResponseResult.NETWORK_ISSUE;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
 | 
			
		||||
 | 
			
		||||
		return ResponseResult.OK;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
 | 
			
		||||
		State tradeBotState = State.valueOf(tradeBotData.getStateValue());
 | 
			
		||||
		if (tradeBotState == null)
 | 
			
		||||
			return true;
 | 
			
		||||
 | 
			
		||||
		// If the AT doesn't exist then we might as well let the user tidy up
 | 
			
		||||
		if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
 | 
			
		||||
			return true;
 | 
			
		||||
 | 
			
		||||
		switch (tradeBotState) {
 | 
			
		||||
			case BOB_WAITING_FOR_AT_CONFIRM:
 | 
			
		||||
			case ALICE_DONE:
 | 
			
		||||
			case BOB_DONE:
 | 
			
		||||
			case ALICE_REFUNDED:
 | 
			
		||||
			case BOB_REFUNDED:
 | 
			
		||||
			case ALICE_REFUNDING_A:
 | 
			
		||||
				return true;
 | 
			
		||||
 | 
			
		||||
			default:
 | 
			
		||||
				return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
 | 
			
		||||
		State tradeBotState = State.valueOf(tradeBotData.getStateValue());
 | 
			
		||||
		if (tradeBotState == null) {
 | 
			
		||||
			LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ATData atData = null;
 | 
			
		||||
		CrossChainTradeData tradeData = null;
 | 
			
		||||
 | 
			
		||||
		if (tradeBotState.requiresAtData) {
 | 
			
		||||
			// Attempt to fetch AT data
 | 
			
		||||
			atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
 | 
			
		||||
			if (atData == null) {
 | 
			
		||||
				LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (tradeBotState.requiresTradeData) {
 | 
			
		||||
				tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
 | 
			
		||||
				if (tradeData == null) {
 | 
			
		||||
					LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch (tradeBotState) {
 | 
			
		||||
			case BOB_WAITING_FOR_AT_CONFIRM:
 | 
			
		||||
				handleBobWaitingForAtConfirm(repository, tradeBotData);
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case BOB_WAITING_FOR_MESSAGE:
 | 
			
		||||
				TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
 | 
			
		||||
				handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case ALICE_WAITING_FOR_AT_LOCK:
 | 
			
		||||
				TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
 | 
			
		||||
				handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case BOB_WAITING_FOR_AT_REDEEM:
 | 
			
		||||
				TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
 | 
			
		||||
				handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case ALICE_DONE:
 | 
			
		||||
			case BOB_DONE:
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case ALICE_REFUNDING_A:
 | 
			
		||||
				TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
 | 
			
		||||
				handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case ALICE_REFUNDED:
 | 
			
		||||
			case BOB_REFUNDED:
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Trade-bot is waiting for Bob's AT to deploy.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
 | 
			
		||||
	 */
 | 
			
		||||
	private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
 | 
			
		||||
		if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
 | 
			
		||||
			if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			// We've waited ages for AT to be confirmed into a block but something has gone awry.
 | 
			
		||||
			// After this long we assume transaction loss so give up with trade-bot entry too.
 | 
			
		||||
			tradeBotData.setState(State.BOB_REFUNDED.name());
 | 
			
		||||
			tradeBotData.setStateValue(State.BOB_REFUNDED.value);
 | 
			
		||||
			tradeBotData.setTimestamp(NTP.getTime());
 | 
			
		||||
			// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
 | 
			
		||||
			repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
 | 
			
		||||
			repository.saveChanges();
 | 
			
		||||
 | 
			
		||||
			LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
 | 
			
		||||
			TradeBot.notifyStateChange(tradeBotData);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
 | 
			
		||||
				() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
 | 
			
		||||
	 * in which case trade-bot is done with this specific trade and finalizes on refunded state.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Assuming P2SH-A has at least expected Digibyte balance,
 | 
			
		||||
	 * Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
 | 
			
		||||
	 * extract secret-A needed to redeem Alice's P2SH.
 | 
			
		||||
	 * @throws ForeignBlockchainException
 | 
			
		||||
	 */
 | 
			
		||||
	private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
 | 
			
		||||
			ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
 | 
			
		||||
		// If AT has finished then Bob likely cancelled his trade offer
 | 
			
		||||
		if (atData.getIsFinished()) {
 | 
			
		||||
			TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
 | 
			
		||||
					() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
 | 
			
		||||
		String address = tradeBotData.getTradeNativeAddress();
 | 
			
		||||
		List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
 | 
			
		||||
 | 
			
		||||
		for (MessageTransactionData messageTransactionData : messageTransactionsData) {
 | 
			
		||||
			if (messageTransactionData.isText())
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			// We're expecting: HASH160(secret-A), Alice's Digibyte pubkeyhash and lockTime-A
 | 
			
		||||
			byte[] messageData = messageTransactionData.getData();
 | 
			
		||||
			DigibyteACCTv3.OfferMessageData offerMessageData = DigibyteACCTv3.extractOfferMessageData(messageData);
 | 
			
		||||
			if (offerMessageData == null)
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDigibytePKH;
 | 
			
		||||
			byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
 | 
			
		||||
			int lockTimeA = (int) offerMessageData.lockTimeA;
 | 
			
		||||
			long messageTimestamp = messageTransactionData.getTimestamp();
 | 
			
		||||
			int refundTimeout = DigibyteACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
 | 
			
		||||
 | 
			
		||||
			// Determine P2SH-A address and confirm funded
 | 
			
		||||
			byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
 | 
			
		||||
			String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
 | 
			
		||||
 | 
			
		||||
			long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
 | 
			
		||||
			long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
 | 
			
		||||
			final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
 | 
			
		||||
 | 
			
		||||
			BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
 | 
			
		||||
 | 
			
		||||
			switch (htlcStatusA) {
 | 
			
		||||
				case UNFUNDED:
 | 
			
		||||
				case FUNDING_IN_PROGRESS:
 | 
			
		||||
					// There might be another MESSAGE from someone else with an actually funded P2SH-A...
 | 
			
		||||
					continue;
 | 
			
		||||
 | 
			
		||||
				case REDEEM_IN_PROGRESS:
 | 
			
		||||
				case REDEEMED:
 | 
			
		||||
					// We've already redeemed this?
 | 
			
		||||
					TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
 | 
			
		||||
							() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
 | 
			
		||||
					return;
 | 
			
		||||
 | 
			
		||||
				case REFUND_IN_PROGRESS:
 | 
			
		||||
				case REFUNDED:
 | 
			
		||||
					// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
 | 
			
		||||
					continue;
 | 
			
		||||
 | 
			
		||||
				case FUNDED:
 | 
			
		||||
					// Fall-through out of switch...
 | 
			
		||||
					break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Good to go - send MESSAGE to AT
 | 
			
		||||
 | 
			
		||||
			String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
 | 
			
		||||
 | 
			
		||||
			// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
 | 
			
		||||
			byte[] outgoingMessageData = DigibyteACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
 | 
			
		||||
			String messageRecipient = tradeBotData.getAtAddress();
 | 
			
		||||
 | 
			
		||||
			boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
 | 
			
		||||
			if (!isMessageAlreadySent) {
 | 
			
		||||
				PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
 | 
			
		||||
				MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
 | 
			
		||||
 | 
			
		||||
				outgoingMessageTransaction.computeNonce();
 | 
			
		||||
				outgoingMessageTransaction.sign(sender);
 | 
			
		||||
 | 
			
		||||
				// reset repository state to prevent deadlock
 | 
			
		||||
				repository.discardChanges();
 | 
			
		||||
				ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
 | 
			
		||||
 | 
			
		||||
				if (result != ValidationResult.OK) {
 | 
			
		||||
					LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
 | 
			
		||||
					() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
 | 
			
		||||
	 * this process has taken so long that we've reached P2SH-A's locktime, or that someone else
 | 
			
		||||
	 * has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * In revealing a valid secret-A, Bob can then redeem the DGB funds from P2SH-A.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * @throws ForeignBlockchainException
 | 
			
		||||
	 */
 | 
			
		||||
	private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
 | 
			
		||||
			ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
 | 
			
		||||
		if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
		int lockTimeA = tradeBotData.getLockTimeA();
 | 
			
		||||
 | 
			
		||||
		// Refund P2SH-A if we've passed lockTime-A
 | 
			
		||||
		if (NTP.getTime() >= lockTimeA * 1000L) {
 | 
			
		||||
			byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
 | 
			
		||||
			String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
 | 
			
		||||
 | 
			
		||||
			long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
 | 
			
		||||
			long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
 | 
			
		||||
			long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
 | 
			
		||||
 | 
			
		||||
			BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
 | 
			
		||||
 | 
			
		||||
			switch (htlcStatusA) {
 | 
			
		||||
				case UNFUNDED:
 | 
			
		||||
				case FUNDING_IN_PROGRESS:
 | 
			
		||||
				case FUNDED:
 | 
			
		||||
					break;
 | 
			
		||||
 | 
			
		||||
				case REDEEM_IN_PROGRESS:
 | 
			
		||||
				case REDEEMED:
 | 
			
		||||
					// Already redeemed?
 | 
			
		||||
					TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
 | 
			
		||||
							() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
 | 
			
		||||
					return;
 | 
			
		||||
 | 
			
		||||
				case REFUND_IN_PROGRESS:
 | 
			
		||||
				case REFUNDED:
 | 
			
		||||
					TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
 | 
			
		||||
							() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
 | 
			
		||||
					return;
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
 | 
			
		||||
					() -> atData.getIsFinished()
 | 
			
		||||
					? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
 | 
			
		||||
					: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// We're waiting for AT to be in TRADE mode
 | 
			
		||||
		if (crossChainTradeData.mode != AcctMode.TRADING)
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
 | 
			
		||||
 | 
			
		||||
		// Find our MESSAGE to AT from previous state
 | 
			
		||||
		List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
 | 
			
		||||
				crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
 | 
			
		||||
		if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
 | 
			
		||||
			LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
 | 
			
		||||
		int refundTimeout = DigibyteACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
 | 
			
		||||
 | 
			
		||||
		// Our calculated refundTimeout should match AT's refundTimeout
 | 
			
		||||
		if (refundTimeout != crossChainTradeData.refundTimeout) {
 | 
			
		||||
			LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
 | 
			
		||||
			// We'll eventually refund
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// We're good to redeem AT
 | 
			
		||||
 | 
			
		||||
		// Send 'redeem' MESSAGE to AT using both secret
 | 
			
		||||
		byte[] secretA = tradeBotData.getSecret();
 | 
			
		||||
		String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
 | 
			
		||||
		byte[] messageData = DigibyteACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
 | 
			
		||||
		String messageRecipient = tradeBotData.getAtAddress();
 | 
			
		||||
 | 
			
		||||
		boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
 | 
			
		||||
		if (!isMessageAlreadySent) {
 | 
			
		||||
			PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
 | 
			
		||||
			MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
 | 
			
		||||
 | 
			
		||||
			messageTransaction.computeNonce();
 | 
			
		||||
			messageTransaction.sign(sender);
 | 
			
		||||
 | 
			
		||||
			// Reset repository state to prevent deadlock
 | 
			
		||||
			repository.discardChanges();
 | 
			
		||||
			ValidationResult result = messageTransaction.importAsUnconfirmed();
 | 
			
		||||
 | 
			
		||||
			if (result != ValidationResult.OK) {
 | 
			
		||||
				LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
 | 
			
		||||
				() -> String.format("Redeeming AT %s. Funds should arrive at %s",
 | 
			
		||||
						tradeBotData.getAtAddress(), qortalReceivingAddress));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DGB funds from P2SH-A.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
 | 
			
		||||
	 * trade-bot is done with this specific trade and finalizes in refunded state.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DGB funds from P2SH-A
 | 
			
		||||
	 * to Bob's 'foreign'/Digibyte trade legacy-format address, as derived from trade private key.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * (This could potentially be 'improved' to send DGB to any address of Bob's choosing by changing the transaction output).
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * If trade-bot successfully broadcasts the transaction, then this specific trade is done.
 | 
			
		||||
	 * @throws ForeignBlockchainException
 | 
			
		||||
	 */
 | 
			
		||||
	private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
 | 
			
		||||
			ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
 | 
			
		||||
		// AT should be 'finished' once Alice has redeemed QORT funds
 | 
			
		||||
		if (!atData.getIsFinished())
 | 
			
		||||
			// Not finished yet
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		// If AT is REFUNDED or CANCELLED then something has gone wrong
 | 
			
		||||
		if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
 | 
			
		||||
			// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DGB
 | 
			
		||||
			TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
 | 
			
		||||
					() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		byte[] secretA = DigibyteACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
 | 
			
		||||
		if (secretA == null) {
 | 
			
		||||
			LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Use secret-A to redeem P2SH-A
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
 | 
			
		||||
		byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
 | 
			
		||||
		int lockTimeA = crossChainTradeData.lockTimeA;
 | 
			
		||||
		byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
 | 
			
		||||
		String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
 | 
			
		||||
 | 
			
		||||
		// Fee for redeem/refund is subtracted from P2SH-A balance.
 | 
			
		||||
		long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
 | 
			
		||||
		long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
 | 
			
		||||
		long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
 | 
			
		||||
		BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
 | 
			
		||||
 | 
			
		||||
		switch (htlcStatusA) {
 | 
			
		||||
			case UNFUNDED:
 | 
			
		||||
			case FUNDING_IN_PROGRESS:
 | 
			
		||||
				// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			case REDEEM_IN_PROGRESS:
 | 
			
		||||
			case REDEEMED:
 | 
			
		||||
				// Double-check that we have redeemed P2SH-A...
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case REFUND_IN_PROGRESS:
 | 
			
		||||
			case REFUNDED:
 | 
			
		||||
				// Wait for AT to auto-refund
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			case FUNDED: {
 | 
			
		||||
				Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
 | 
			
		||||
				ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
 | 
			
		||||
				List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
 | 
			
		||||
 | 
			
		||||
				Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(digibyte.getNetworkParameters(), redeemAmount, redeemKey,
 | 
			
		||||
						fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
 | 
			
		||||
 | 
			
		||||
				digibyte.broadcastTransaction(p2shRedeemTransaction);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		String receivingAddress = digibyte.pkhToAddress(receivingAccountInfo);
 | 
			
		||||
 | 
			
		||||
		TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
 | 
			
		||||
				() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Trade-bot is attempting to refund P2SH-A.
 | 
			
		||||
	 * @throws ForeignBlockchainException
 | 
			
		||||
	 */
 | 
			
		||||
	private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
 | 
			
		||||
			ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
 | 
			
		||||
		int lockTimeA = tradeBotData.getLockTimeA();
 | 
			
		||||
 | 
			
		||||
		// We can't refund P2SH-A until lockTime-A has passed
 | 
			
		||||
		if (NTP.getTime() <= lockTimeA * 1000L)
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		Digibyte digibyte = Digibyte.getInstance();
 | 
			
		||||
 | 
			
		||||
		// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
 | 
			
		||||
		int medianBlockTime = digibyte.getMedianBlockTime();
 | 
			
		||||
		if (medianBlockTime <= lockTimeA)
 | 
			
		||||
			return;
 | 
			
		||||
 | 
			
		||||
		byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
 | 
			
		||||
		String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
 | 
			
		||||
 | 
			
		||||
		// Fee for redeem/refund is subtracted from P2SH-A balance.
 | 
			
		||||
		long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
 | 
			
		||||
		long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
 | 
			
		||||
		long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
 | 
			
		||||
		BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
 | 
			
		||||
 | 
			
		||||
		switch (htlcStatusA) {
 | 
			
		||||
			case UNFUNDED:
 | 
			
		||||
			case FUNDING_IN_PROGRESS:
 | 
			
		||||
				// Still waiting for P2SH-A to be funded...
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			case REDEEM_IN_PROGRESS:
 | 
			
		||||
			case REDEEMED:
 | 
			
		||||
				// Too late!
 | 
			
		||||
				TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
 | 
			
		||||
						() -> String.format("P2SH-A %s already spent!", p2shAddressA));
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			case REFUND_IN_PROGRESS:
 | 
			
		||||
			case REFUNDED:
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case FUNDED:{
 | 
			
		||||
				Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
 | 
			
		||||
				ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
 | 
			
		||||
				List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
 | 
			
		||||
 | 
			
		||||
				// Determine receive address for refund
 | 
			
		||||
				String receiveAddress = digibyte.getUnusedReceiveAddress(tradeBotData.getForeignKey());
 | 
			
		||||
				Address receiving = Address.fromString(digibyte.getNetworkParameters(), receiveAddress);
 | 
			
		||||
 | 
			
		||||
				Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(digibyte.getNetworkParameters(), refundAmount, refundKey,
 | 
			
		||||
						fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
 | 
			
		||||
 | 
			
		||||
				digibyte.broadcastTransaction(p2shRefundTransaction);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
 | 
			
		||||
				() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @throws DataException
 | 
			
		||||
	 * @throws ForeignBlockchainException
 | 
			
		||||
	 */
 | 
			
		||||
	private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
 | 
			
		||||
			ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
 | 
			
		||||
		// This is OK
 | 
			
		||||
		if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
 | 
			
		||||
			return false;
 | 
			
		||||
 | 
			
		||||
		boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
 | 
			
		||||
 | 
			
		||||
		if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
 | 
			
		||||
			if (isAtLockedToUs) {
 | 
			
		||||
				// AT is trading with us - OK
 | 
			
		||||
				return false;
 | 
			
		||||
			} else {
 | 
			
		||||
				TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
 | 
			
		||||
						() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
 | 
			
		||||
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
 | 
			
		||||
			// We've redeemed already?
 | 
			
		||||
			TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
 | 
			
		||||
					() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
 | 
			
		||||
		} else {
 | 
			
		||||
			// Any other state is not good, so start defensive refund
 | 
			
		||||
			TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
 | 
			
		||||
					() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
 | 
			
		||||
		return (lockTimeA - tradeTimeout * 60) * 1000L;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -100,6 +100,7 @@ public class TradeBot implements Listener {
 | 
			
		||||
		acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
 | 
			
		||||
		acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
 | 
			
		||||
		acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
 | 
			
		||||
		acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static TradeBot instance;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										171
									
								
								src/main/java/org/qortal/crosschain/Digibyte.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								src/main/java/org/qortal/crosschain/Digibyte.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,171 @@
 | 
			
		||||
package org.qortal.crosschain;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.EnumMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import org.bitcoinj.core.Coin;
 | 
			
		||||
import org.bitcoinj.core.Context;
 | 
			
		||||
import org.bitcoinj.core.NetworkParameters;
 | 
			
		||||
import org.bitcoinj.params.RegTestParams;
 | 
			
		||||
import org.bitcoinj.params.TestNet3Params;
 | 
			
		||||
import org.libdohj.params.DigibyteMainNetParams;
 | 
			
		||||
import org.qortal.crosschain.ElectrumX.Server;
 | 
			
		||||
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
 | 
			
		||||
import org.qortal.settings.Settings;
 | 
			
		||||
 | 
			
		||||
public class Digibyte extends Bitcoiny {
 | 
			
		||||
 | 
			
		||||
	public static final String CURRENCY_CODE = "DGB";
 | 
			
		||||
 | 
			
		||||
	private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(100000); // 0.001 DGB per 1000 bytes
 | 
			
		||||
 | 
			
		||||
	private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 DGB minimum order, to avoid dust errors
 | 
			
		||||
 | 
			
		||||
	// Temporary values until a dynamic fee system is written.
 | 
			
		||||
	private static final long MAINNET_FEE = 10000L;
 | 
			
		||||
	private static final long NON_MAINNET_FEE = 10000L; // enough for TESTNET3 and should be OK for REGTEST
 | 
			
		||||
 | 
			
		||||
	private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
 | 
			
		||||
	static {
 | 
			
		||||
		DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
 | 
			
		||||
		DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public enum DigibyteNet {
 | 
			
		||||
		MAIN {
 | 
			
		||||
			@Override
 | 
			
		||||
			public NetworkParameters getParams() {
 | 
			
		||||
				return DigibyteMainNetParams.get();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public Collection<Server> getServers() {
 | 
			
		||||
				return Arrays.asList(
 | 
			
		||||
						// Servers chosen on NO BASIS WHATSOEVER from various sources!
 | 
			
		||||
						// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
 | 
			
		||||
						new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
 | 
			
		||||
						new Server("electrum2.cipig.net", ConnectionType.SSL, 20059),
 | 
			
		||||
						new Server("electrum3.cipig.net", ConnectionType.SSL, 20059));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public String getGenesisHash() {
 | 
			
		||||
				return "7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496";
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public long getP2shFee(Long timestamp) {
 | 
			
		||||
				// TODO: This will need to be replaced with something better in the near future!
 | 
			
		||||
				return MAINNET_FEE;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		TEST3 {
 | 
			
		||||
			@Override
 | 
			
		||||
			public NetworkParameters getParams() {
 | 
			
		||||
				return TestNet3Params.get();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public Collection<Server> getServers() {
 | 
			
		||||
				return Arrays.asList(); // TODO: find testnet servers
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public String getGenesisHash() {
 | 
			
		||||
				return "308ea0711d5763be2995670dd9ca9872753561285a84da1d58be58acaa822252";
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public long getP2shFee(Long timestamp) {
 | 
			
		||||
				return NON_MAINNET_FEE;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		REGTEST {
 | 
			
		||||
			@Override
 | 
			
		||||
			public NetworkParameters getParams() {
 | 
			
		||||
				return RegTestParams.get();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public Collection<Server> getServers() {
 | 
			
		||||
				return Arrays.asList(
 | 
			
		||||
						new Server("localhost", ConnectionType.TCP, 50001),
 | 
			
		||||
						new Server("localhost", ConnectionType.SSL, 50002));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public String getGenesisHash() {
 | 
			
		||||
				// This is unique to each regtest instance
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public long getP2shFee(Long timestamp) {
 | 
			
		||||
				return NON_MAINNET_FEE;
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		public abstract NetworkParameters getParams();
 | 
			
		||||
		public abstract Collection<Server> getServers();
 | 
			
		||||
		public abstract String getGenesisHash();
 | 
			
		||||
		public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static Digibyte instance;
 | 
			
		||||
 | 
			
		||||
	private final DigibyteNet digibyteNet;
 | 
			
		||||
 | 
			
		||||
	// Constructors and instance
 | 
			
		||||
 | 
			
		||||
	private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
 | 
			
		||||
		super(blockchain, bitcoinjContext, currencyCode);
 | 
			
		||||
		this.digibyteNet = digibyteNet;
 | 
			
		||||
 | 
			
		||||
		LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static synchronized Digibyte getInstance() {
 | 
			
		||||
		if (instance == null) {
 | 
			
		||||
			DigibyteNet digibyteNet = Settings.getInstance().getDigibyteNet();
 | 
			
		||||
 | 
			
		||||
			BitcoinyBlockchainProvider electrumX = new ElectrumX("Digibyte-" + digibyteNet.name(), digibyteNet.getGenesisHash(), digibyteNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
 | 
			
		||||
			Context bitcoinjContext = new Context(digibyteNet.getParams());
 | 
			
		||||
 | 
			
		||||
			instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return instance;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Getters & setters
 | 
			
		||||
 | 
			
		||||
	public static synchronized void resetForTesting() {
 | 
			
		||||
		instance = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Actual useful methods for use by other classes
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Coin getFeePerKb() {
 | 
			
		||||
		return DEFAULT_FEE_PER_KB;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public long getMinimumOrderAmount() {
 | 
			
		||||
		return MINIMUM_ORDER_AMOUNT;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns estimated DGB fee, in sats per 1000bytes, optionally for historic timestamp.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param timestamp optional milliseconds since epoch, or null for 'now'
 | 
			
		||||
	 * @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
 | 
			
		||||
		return this.digibyteNet.getP2shFee(timestamp);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										858
									
								
								src/main/java/org/qortal/crosschain/DigibyteACCTv3.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										858
									
								
								src/main/java/org/qortal/crosschain/DigibyteACCTv3.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,858 @@
 | 
			
		||||
package org.qortal.crosschain;
 | 
			
		||||
 | 
			
		||||
import com.google.common.hash.HashCode;
 | 
			
		||||
import com.google.common.primitives.Bytes;
 | 
			
		||||
import org.apache.logging.log4j.LogManager;
 | 
			
		||||
import org.apache.logging.log4j.Logger;
 | 
			
		||||
import org.ciyam.at.*;
 | 
			
		||||
import org.qortal.account.Account;
 | 
			
		||||
import org.qortal.asset.Asset;
 | 
			
		||||
import org.qortal.at.QortalFunctionCode;
 | 
			
		||||
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.MessageTransactionData;
 | 
			
		||||
import org.qortal.repository.DataException;
 | 
			
		||||
import org.qortal.repository.Repository;
 | 
			
		||||
import org.qortal.utils.Base58;
 | 
			
		||||
import org.qortal.utils.BitTwiddling;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import static org.ciyam.at.OpCode.calcOffset;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Cross-chain trade AT
 | 
			
		||||
 * 
 | 
			
		||||
 * <p>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 * <li>Bob generates Digibyte & Qortal 'trade' keys
 | 
			
		||||
 * 		<ul>
 | 
			
		||||
 * 			<li>private key required to sign P2SH redeem tx</li>
 | 
			
		||||
 * 			<li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
 | 
			
		||||
 * 			<li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
 | 
			
		||||
 * 		</ul>
 | 
			
		||||
 * </li>
 | 
			
		||||
 * <li>Bob deploys Qortal AT
 | 
			
		||||
 * 		<ul>
 | 
			
		||||
 * 		</ul>
 | 
			
		||||
 * </li>
 | 
			
		||||
 * <li>Alice finds Qortal AT and wants to trade
 | 
			
		||||
 * 		<ul>
 | 
			
		||||
 * 			<li>Alice generates Digibyte & Qortal 'trade' keys</li>
 | 
			
		||||
 * 			<li>Alice funds Digibyte P2SH-A</li>
 | 
			
		||||
 * 			<li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
 | 
			
		||||
 * 				<ul>
 | 
			
		||||
 * 					<li>hash-of-secret-A</li>
 | 
			
		||||
 * 					<li>her 'trade' Digibyte PKH</li>
 | 
			
		||||
 * 				</ul>
 | 
			
		||||
 * 			</li>
 | 
			
		||||
 * 		</ul>
 | 
			
		||||
 * </li>
 | 
			
		||||
 * <li>Bob receives "offer" MESSAGE
 | 
			
		||||
 * 		<ul>
 | 
			
		||||
 * 			<li>Checks Alice's P2SH-A</li>
 | 
			
		||||
 * 			<li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
 | 
			
		||||
 * 				<ul>
 | 
			
		||||
 * 					<li>Alice's trade Qortal address</li>
 | 
			
		||||
 * 					<li>Alice's trade Digibyte PKH</li>
 | 
			
		||||
 * 					<li>hash-of-secret-A</li>
 | 
			
		||||
 * 				</ul>
 | 
			
		||||
 * 			</li>
 | 
			
		||||
 * 		</ul>
 | 
			
		||||
 * </li>
 | 
			
		||||
 * <li>Alice checks Qortal AT to confirm it's locked to her
 | 
			
		||||
 * 		<ul>
 | 
			
		||||
 * 			<li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
 | 
			
		||||
 * 				<ul>
 | 
			
		||||
 * 					<li>secret-A</li>
 | 
			
		||||
 * 					<li>Qortal receiving address of her chosing</li>
 | 
			
		||||
 * 				</ul>
 | 
			
		||||
 * 			</li>
 | 
			
		||||
 * 			<li>AT's QORT funds are sent to Qortal receiving address</li>
 | 
			
		||||
 * 		</ul>
 | 
			
		||||
 * </li>
 | 
			
		||||
 * <li>Bob checks AT, extracts secret-A
 | 
			
		||||
 * 		<ul>
 | 
			
		||||
 * 			<li>Bob redeems P2SH-A using his Digibyte trade key and secret-A</li>
 | 
			
		||||
 * 			<li>P2SH-A DGB funds end up at Digibyte address determined by redeem transaction output(s)</li>
 | 
			
		||||
 * 		</ul>
 | 
			
		||||
 * </li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 */
 | 
			
		||||
public class DigibyteACCTv3 implements ACCT {
 | 
			
		||||
 | 
			
		||||
	private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3.class);
 | 
			
		||||
 | 
			
		||||
	public static final String NAME = DigibyteACCTv3.class.getSimpleName();
 | 
			
		||||
	public static final byte[] CODE_BYTES_HASH = HashCode.fromString("e6a7dcd87296fae3ce7d80183bf7660c8e2cb4f8746c6a0421a17148f87a0e1d").asBytes(); // SHA256 of AT code bytes
 | 
			
		||||
 | 
			
		||||
	public static final int SECRET_LENGTH = 32;
 | 
			
		||||
 | 
			
		||||
	/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
 | 
			
		||||
	private static final int MODE_VALUE_OFFSET = 61;
 | 
			
		||||
	/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
 | 
			
		||||
	public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
 | 
			
		||||
 | 
			
		||||
	public static class OfferMessageData {
 | 
			
		||||
		public byte[] partnerDigibytePKH;
 | 
			
		||||
		public byte[] hashOfSecretA;
 | 
			
		||||
		public long lockTimeA;
 | 
			
		||||
	}
 | 
			
		||||
	public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDigibytePKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
 | 
			
		||||
	public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
 | 
			
		||||
			+ 24 /*partner's Digibyte PKH (padded from 20 to 24)*/
 | 
			
		||||
			+ 8 /*AT trade timeout (minutes)*/
 | 
			
		||||
			+ 24 /*hash of secret-A (padded from 20 to 24)*/
 | 
			
		||||
			+ 8 /*lockTimeA*/;
 | 
			
		||||
	public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
 | 
			
		||||
	public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
 | 
			
		||||
 | 
			
		||||
	private static DigibyteACCTv3 instance;
 | 
			
		||||
 | 
			
		||||
	private DigibyteACCTv3() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static synchronized DigibyteACCTv3 getInstance() {
 | 
			
		||||
		if (instance == null)
 | 
			
		||||
			instance = new DigibyteACCTv3();
 | 
			
		||||
 | 
			
		||||
		return instance;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public byte[] getCodeBytesHash() {
 | 
			
		||||
		return CODE_BYTES_HASH;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int getModeByteOffset() {
 | 
			
		||||
		return MODE_BYTE_OFFSET;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public ForeignBlockchain getBlockchain() {
 | 
			
		||||
		return Digibyte.getInstance();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns Qortal AT creation bytes for cross-chain trading AT.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 * <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the
 | 
			
		||||
	 * 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param creatorTradeAddress AT creator's trade Qortal address
 | 
			
		||||
	 * @param digibytePublicKeyHash 20-byte HASH160 of creator's trade Digibyte public key
 | 
			
		||||
	 * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
 | 
			
		||||
	 * @param digibyteAmount how much DGB the AT creator is expecting to trade
 | 
			
		||||
	 * @param tradeTimeout suggested timeout for entire trade
 | 
			
		||||
	 */
 | 
			
		||||
	public static byte[] buildQortalAT(String creatorTradeAddress, byte[] digibytePublicKeyHash, long qortAmount, long digibyteAmount, int tradeTimeout) {
 | 
			
		||||
		if (digibytePublicKeyHash.length != 20)
 | 
			
		||||
			throw new IllegalArgumentException("Digibyte public key hash should be 20 bytes");
 | 
			
		||||
 | 
			
		||||
		// Labels for data segment addresses
 | 
			
		||||
		int addrCounter = 0;
 | 
			
		||||
 | 
			
		||||
		// Constants (with corresponding dataByteBuffer.put*() calls below)
 | 
			
		||||
 | 
			
		||||
		final int addrCreatorTradeAddress1 = addrCounter++;
 | 
			
		||||
		final int addrCreatorTradeAddress2 = addrCounter++;
 | 
			
		||||
		final int addrCreatorTradeAddress3 = addrCounter++;
 | 
			
		||||
		final int addrCreatorTradeAddress4 = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrDigibytePublicKeyHash = addrCounter;
 | 
			
		||||
		addrCounter += 4;
 | 
			
		||||
 | 
			
		||||
		final int addrQortAmount = addrCounter++;
 | 
			
		||||
		final int addrDigibyteAmount = addrCounter++;
 | 
			
		||||
		final int addrTradeTimeout = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrMessageTxnType = addrCounter++;
 | 
			
		||||
		final int addrExpectedTradeMessageLength = addrCounter++;
 | 
			
		||||
		final int addrExpectedRedeemMessageLength = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrCreatorAddressPointer = addrCounter++;
 | 
			
		||||
		final int addrQortalPartnerAddressPointer = addrCounter++;
 | 
			
		||||
		final int addrMessageSenderPointer = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrTradeMessagePartnerDigibytePKHOffset = addrCounter++;
 | 
			
		||||
		final int addrPartnerDigibytePKHPointer = addrCounter++;
 | 
			
		||||
		final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
 | 
			
		||||
		final int addrHashOfSecretAPointer = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrMessageDataPointer = addrCounter++;
 | 
			
		||||
		final int addrMessageDataLength = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrPartnerReceivingAddressPointer = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrEndOfConstants = addrCounter;
 | 
			
		||||
 | 
			
		||||
		// Variables
 | 
			
		||||
 | 
			
		||||
		final int addrCreatorAddress1 = addrCounter++;
 | 
			
		||||
		final int addrCreatorAddress2 = addrCounter++;
 | 
			
		||||
		final int addrCreatorAddress3 = addrCounter++;
 | 
			
		||||
		final int addrCreatorAddress4 = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrQortalPartnerAddress1 = addrCounter++;
 | 
			
		||||
		final int addrQortalPartnerAddress2 = addrCounter++;
 | 
			
		||||
		final int addrQortalPartnerAddress3 = addrCounter++;
 | 
			
		||||
		final int addrQortalPartnerAddress4 = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrLockTimeA = addrCounter++;
 | 
			
		||||
		final int addrRefundTimeout = addrCounter++;
 | 
			
		||||
		final int addrRefundTimestamp = addrCounter++;
 | 
			
		||||
		final int addrLastTxnTimestamp = addrCounter++;
 | 
			
		||||
		final int addrBlockTimestamp = addrCounter++;
 | 
			
		||||
		final int addrTxnType = addrCounter++;
 | 
			
		||||
		final int addrResult = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrMessageSender1 = addrCounter++;
 | 
			
		||||
		final int addrMessageSender2 = addrCounter++;
 | 
			
		||||
		final int addrMessageSender3 = addrCounter++;
 | 
			
		||||
		final int addrMessageSender4 = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrMessageLength = addrCounter++;
 | 
			
		||||
 | 
			
		||||
		final int addrMessageData = addrCounter;
 | 
			
		||||
		addrCounter += 4;
 | 
			
		||||
 | 
			
		||||
		final int addrHashOfSecretA = addrCounter;
 | 
			
		||||
		addrCounter += 4;
 | 
			
		||||
 | 
			
		||||
		final int addrPartnerDigibytePKH = addrCounter;
 | 
			
		||||
		addrCounter += 4;
 | 
			
		||||
 | 
			
		||||
		final int addrPartnerReceivingAddress = addrCounter;
 | 
			
		||||
		addrCounter += 4;
 | 
			
		||||
 | 
			
		||||
		final int addrMode = addrCounter++;
 | 
			
		||||
		assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
 | 
			
		||||
 | 
			
		||||
		// Data segment
 | 
			
		||||
		ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
 | 
			
		||||
 | 
			
		||||
		// AT creator's trade Qortal address, decoded from Base58
 | 
			
		||||
		assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
 | 
			
		||||
		byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
 | 
			
		||||
		dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
 | 
			
		||||
 | 
			
		||||
		// Digibyte public key hash
 | 
			
		||||
		assert dataByteBuffer.position() == addrDigibytePublicKeyHash * MachineState.VALUE_SIZE : "addrDigibytePublicKeyHash incorrect";
 | 
			
		||||
		dataByteBuffer.put(Bytes.ensureCapacity(digibytePublicKeyHash, 32, 0));
 | 
			
		||||
 | 
			
		||||
		// Redeem Qort amount
 | 
			
		||||
		assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(qortAmount);
 | 
			
		||||
 | 
			
		||||
		// Expected Digibyte amount
 | 
			
		||||
		assert dataByteBuffer.position() == addrDigibyteAmount * MachineState.VALUE_SIZE : "addrDigibyteAmount incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(digibyteAmount);
 | 
			
		||||
 | 
			
		||||
		// Suggested trade timeout (minutes)
 | 
			
		||||
		assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(tradeTimeout);
 | 
			
		||||
 | 
			
		||||
		// We're only interested in MESSAGE transactions
 | 
			
		||||
		assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
 | 
			
		||||
 | 
			
		||||
		// Expected length of 'trade' MESSAGE data from AT creator
 | 
			
		||||
		assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
 | 
			
		||||
 | 
			
		||||
		// Expected length of 'redeem' MESSAGE data from trade partner
 | 
			
		||||
		assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
 | 
			
		||||
 | 
			
		||||
		// Index into data segment of AT creator's address, used by GET_B_IND
 | 
			
		||||
		assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrCreatorAddress1);
 | 
			
		||||
 | 
			
		||||
		// Index into data segment of partner's Qortal address, used by SET_B_IND
 | 
			
		||||
		assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrQortalPartnerAddress1);
 | 
			
		||||
 | 
			
		||||
		// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
 | 
			
		||||
		assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrMessageSender1);
 | 
			
		||||
 | 
			
		||||
		// Offset into 'trade' MESSAGE data payload for extracting partner's Digibyte PKH
 | 
			
		||||
		assert dataByteBuffer.position() == addrTradeMessagePartnerDigibytePKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDigibytePKHOffset incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(32L);
 | 
			
		||||
 | 
			
		||||
		// Index into data segment of partner's Digibyte PKH, used by GET_B_IND
 | 
			
		||||
		assert dataByteBuffer.position() == addrPartnerDigibytePKHPointer * MachineState.VALUE_SIZE : "addrPartnerDigibytePKHPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrPartnerDigibytePKH);
 | 
			
		||||
 | 
			
		||||
		// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
 | 
			
		||||
		assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(64L);
 | 
			
		||||
 | 
			
		||||
		// Index into data segment to hash of secret A, used by GET_B_IND
 | 
			
		||||
		assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrHashOfSecretA);
 | 
			
		||||
 | 
			
		||||
		// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
 | 
			
		||||
		assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(32L);
 | 
			
		||||
 | 
			
		||||
		// Source location and length for hashing any passed secret
 | 
			
		||||
		assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrMessageData);
 | 
			
		||||
		assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(32L);
 | 
			
		||||
 | 
			
		||||
		// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
 | 
			
		||||
		assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
 | 
			
		||||
		dataByteBuffer.putLong(addrPartnerReceivingAddress);
 | 
			
		||||
 | 
			
		||||
		assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
 | 
			
		||||
 | 
			
		||||
		// Code labels
 | 
			
		||||
		Integer labelRefund = null;
 | 
			
		||||
 | 
			
		||||
		Integer labelTradeTxnLoop = null;
 | 
			
		||||
		Integer labelCheckTradeTxn = null;
 | 
			
		||||
		Integer labelCheckCancelTxn = null;
 | 
			
		||||
		Integer labelNotTradeNorCancelTxn = null;
 | 
			
		||||
		Integer labelCheckNonRefundTradeTxn = null;
 | 
			
		||||
		Integer labelTradeTxnExtract = null;
 | 
			
		||||
		Integer labelRedeemTxnLoop = null;
 | 
			
		||||
		Integer labelCheckRedeemTxn = null;
 | 
			
		||||
		Integer labelCheckRedeemTxnSender = null;
 | 
			
		||||
		Integer labelPayout = null;
 | 
			
		||||
 | 
			
		||||
		ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
 | 
			
		||||
 | 
			
		||||
		// Two-pass version
 | 
			
		||||
		for (int pass = 0; pass < 2; ++pass) {
 | 
			
		||||
			codeByteBuffer.clear();
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
				/* Initialization */
 | 
			
		||||
 | 
			
		||||
				// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
 | 
			
		||||
 | 
			
		||||
				/* NOP - to ensure DIGIBYTE ACCT is unique */
 | 
			
		||||
				codeByteBuffer.put(OpCode.NOP.compile());
 | 
			
		||||
 | 
			
		||||
				// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
 | 
			
		||||
 | 
			
		||||
				// Set restart position to after this opcode
 | 
			
		||||
				codeByteBuffer.put(OpCode.SET_PCS.compile());
 | 
			
		||||
 | 
			
		||||
				/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
 | 
			
		||||
 | 
			
		||||
				/* Transaction processing loop */
 | 
			
		||||
				labelTradeTxnLoop = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				/* Sleep until message arrives */
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
 | 
			
		||||
 | 
			
		||||
				// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
 | 
			
		||||
				// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
 | 
			
		||||
				// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
 | 
			
		||||
				// Stop and wait for next block
 | 
			
		||||
				codeByteBuffer.put(OpCode.STP_IMD.compile());
 | 
			
		||||
 | 
			
		||||
				/* Check transaction */
 | 
			
		||||
				labelCheckTradeTxn = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
 | 
			
		||||
				// Extract transaction type (message/payment) from transaction and save type in addrTxnType
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
 | 
			
		||||
				// If transaction type is not MESSAGE type then go look for another transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
 | 
			
		||||
 | 
			
		||||
				/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
 | 
			
		||||
 | 
			
		||||
				// Extract sender address from transaction into B register
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
 | 
			
		||||
				// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
 | 
			
		||||
				// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
 | 
			
		||||
				// Message sender's address matches AT creator's trade address so go process 'trade' message
 | 
			
		||||
				codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
 | 
			
		||||
 | 
			
		||||
				/* Checking message sender for possible cancel message */
 | 
			
		||||
				labelCheckCancelTxn = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
 | 
			
		||||
				// Partner address is AT creator's address, so cancel offer and finish.
 | 
			
		||||
				codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
 | 
			
		||||
				// We're finished forever (finishing auto-refunds remaining balance to AT creator)
 | 
			
		||||
				codeByteBuffer.put(OpCode.FIN_IMD.compile());
 | 
			
		||||
 | 
			
		||||
				/* Not trade nor cancel message */
 | 
			
		||||
				labelNotTradeNorCancelTxn = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Loop to find another transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
 | 
			
		||||
 | 
			
		||||
				/* Possible switch-to-trade-mode message */
 | 
			
		||||
				labelCheckNonRefundTradeTxn = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Check 'trade' message we received has expected number of message bytes
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
 | 
			
		||||
				// If message length matches, branch to info extraction code
 | 
			
		||||
				codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
 | 
			
		||||
				// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
 | 
			
		||||
 | 
			
		||||
				/* Extracting info from 'trade' MESSAGE transaction */
 | 
			
		||||
				labelTradeTxnExtract = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Extract message from transaction into B register
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
 | 
			
		||||
				// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
 | 
			
		||||
 | 
			
		||||
				// Extract trade partner's Digibyte public key hash (PKH) from message into B
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDigibytePKHOffset));
 | 
			
		||||
				// Store partner's Digibyte PKH (we only really use values from B1-B3)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDigibytePKHPointer));
 | 
			
		||||
				// Extract AT trade timeout (minutes) (from B4)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
 | 
			
		||||
 | 
			
		||||
				// Grab next 32 bytes
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
 | 
			
		||||
 | 
			
		||||
				// Extract hash-of-secret-A (we only really use values from B1-B3)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
 | 
			
		||||
				// Extract lockTime-A (from B4)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
 | 
			
		||||
 | 
			
		||||
				// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
 | 
			
		||||
 | 
			
		||||
				/* We are in 'trade mode' */
 | 
			
		||||
				codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
 | 
			
		||||
 | 
			
		||||
				// Set restart position to after this opcode
 | 
			
		||||
				codeByteBuffer.put(OpCode.SET_PCS.compile());
 | 
			
		||||
 | 
			
		||||
				/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
 | 
			
		||||
 | 
			
		||||
				// Fetch current block 'timestamp'
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
 | 
			
		||||
				// If we're not past refund 'timestamp' then look for next transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
 | 
			
		||||
				// We're past refund 'timestamp' so go refund everything back to AT creator
 | 
			
		||||
				codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
 | 
			
		||||
 | 
			
		||||
				/* Transaction processing loop */
 | 
			
		||||
				labelRedeemTxnLoop = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Find next transaction to this AT since the last one (if any)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
 | 
			
		||||
				// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
 | 
			
		||||
				// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
 | 
			
		||||
				// Stop and wait for next block
 | 
			
		||||
				codeByteBuffer.put(OpCode.STP_IMD.compile());
 | 
			
		||||
 | 
			
		||||
				/* Check transaction */
 | 
			
		||||
				labelCheckRedeemTxn = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
 | 
			
		||||
				// Extract transaction type (message/payment) from transaction and save type in addrTxnType
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
 | 
			
		||||
				// If transaction type is not MESSAGE type then go look for another transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
 | 
			
		||||
 | 
			
		||||
				/* Check message payload length */
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
 | 
			
		||||
				// If message length matches, branch to sender checking code
 | 
			
		||||
				codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
 | 
			
		||||
				// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
 | 
			
		||||
 | 
			
		||||
				/* Check transaction's sender */
 | 
			
		||||
				labelCheckRedeemTxnSender = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Extract sender address from transaction into B register
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
 | 
			
		||||
				// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
 | 
			
		||||
				// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
 | 
			
		||||
 | 
			
		||||
				/* Check 'secret-A' in transaction's message */
 | 
			
		||||
 | 
			
		||||
				// Extract secret-A from first 32 bytes of message from transaction into B register
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
 | 
			
		||||
				// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
 | 
			
		||||
				// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
 | 
			
		||||
				// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
 | 
			
		||||
				// Save the equality result (1 if they match, 0 otherwise) into addrResult.
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
 | 
			
		||||
				// If hashes don't match, addrResult will be zero so go find another transaction
 | 
			
		||||
				codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
 | 
			
		||||
				codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
 | 
			
		||||
 | 
			
		||||
				/* Success! Pay arranged amount to receiving address */
 | 
			
		||||
				labelPayout = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
 | 
			
		||||
				// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
 | 
			
		||||
				// Pay AT's balance to receiving address
 | 
			
		||||
				codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
 | 
			
		||||
				// Set redeemed mode
 | 
			
		||||
				codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
 | 
			
		||||
				// We're finished forever (finishing auto-refunds remaining balance to AT creator)
 | 
			
		||||
				codeByteBuffer.put(OpCode.FIN_IMD.compile());
 | 
			
		||||
 | 
			
		||||
				// Fall-through to refunding any remaining balance back to AT creator
 | 
			
		||||
 | 
			
		||||
				/* Refund balance back to AT creator */
 | 
			
		||||
				labelRefund = codeByteBuffer.position();
 | 
			
		||||
 | 
			
		||||
				// Set refunded mode
 | 
			
		||||
				codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
 | 
			
		||||
				// We're finished forever (finishing auto-refunds remaining balance to AT creator)
 | 
			
		||||
				codeByteBuffer.put(OpCode.FIN_IMD.compile());
 | 
			
		||||
			} catch (CompilationException e) {
 | 
			
		||||
				throw new IllegalStateException("Unable to compile DGB-QORT ACCT?", e);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		codeByteBuffer.flip();
 | 
			
		||||
 | 
			
		||||
		byte[] codeBytes = new byte[codeByteBuffer.limit()];
 | 
			
		||||
		codeByteBuffer.get(codeBytes);
 | 
			
		||||
 | 
			
		||||
		assert Arrays.equals(Crypto.digest(codeBytes), DigibyteACCTv3.CODE_BYTES_HASH)
 | 
			
		||||
			: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
 | 
			
		||||
 | 
			
		||||
		final short ciyamAtVersion = 2;
 | 
			
		||||
		final short numCallStackPages = 0;
 | 
			
		||||
		final short numUserStackPages = 0;
 | 
			
		||||
		final long minActivationAmount = 0L;
 | 
			
		||||
 | 
			
		||||
		return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns CrossChainTradeData with useful info extracted from AT.
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
 | 
			
		||||
		ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
 | 
			
		||||
		return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns CrossChainTradeData with useful info extracted from AT.
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
 | 
			
		||||
		ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
 | 
			
		||||
		return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns CrossChainTradeData with useful info extracted from AT.
 | 
			
		||||
	 */
 | 
			
		||||
	public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
 | 
			
		||||
		byte[] addressBytes = new byte[25]; // for general use
 | 
			
		||||
		String atAddress = atStateData.getATAddress();
 | 
			
		||||
 | 
			
		||||
		CrossChainTradeData tradeData = new CrossChainTradeData();
 | 
			
		||||
 | 
			
		||||
		tradeData.foreignBlockchain = SupportedBlockchain.DIGIBYTE.name();
 | 
			
		||||
		tradeData.acctName = NAME;
 | 
			
		||||
 | 
			
		||||
		tradeData.qortalAtAddress = atAddress;
 | 
			
		||||
		tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
 | 
			
		||||
		tradeData.creationTimestamp = creationTimestamp;
 | 
			
		||||
 | 
			
		||||
		Account atAccount = new Account(repository, atAddress);
 | 
			
		||||
		tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
 | 
			
		||||
 | 
			
		||||
		byte[] stateData = atStateData.getStateData();
 | 
			
		||||
		ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
 | 
			
		||||
		dataByteBuffer.position(MachineState.HEADER_LENGTH);
 | 
			
		||||
 | 
			
		||||
		/* Constants */
 | 
			
		||||
 | 
			
		||||
		// Skip creator's trade address
 | 
			
		||||
		dataByteBuffer.get(addressBytes);
 | 
			
		||||
		tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
 | 
			
		||||
 | 
			
		||||
		// Creator's Digibyte/foreign public key hash
 | 
			
		||||
		tradeData.creatorForeignPKH = new byte[20];
 | 
			
		||||
		dataByteBuffer.get(tradeData.creatorForeignPKH);
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
 | 
			
		||||
 | 
			
		||||
		// We don't use secret-B
 | 
			
		||||
		tradeData.hashOfSecretB = null;
 | 
			
		||||
 | 
			
		||||
		// Redeem payout
 | 
			
		||||
		tradeData.qortAmount = dataByteBuffer.getLong();
 | 
			
		||||
 | 
			
		||||
		// Expected DGB amount
 | 
			
		||||
		tradeData.expectedForeignAmount = dataByteBuffer.getLong();
 | 
			
		||||
 | 
			
		||||
		// Trade timeout
 | 
			
		||||
		tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
 | 
			
		||||
 | 
			
		||||
		// Skip MESSAGE transaction type
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip expected 'trade' message length
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip expected 'redeem' message length
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to creator's address
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to partner's Qortal trade address
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to message sender
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip 'trade' message data offset for partner's Digibyte PKH
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to partner's Digibyte PKH
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip 'trade' message data offset for hash-of-secret-A
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to hash-of-secret-A
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip 'redeem' message data offset for partner's Qortal receiving address
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to message data
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip message data length
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip pointer to partner's receiving address
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		/* End of constants / begin variables */
 | 
			
		||||
 | 
			
		||||
		// Skip AT creator's address
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
 | 
			
		||||
 | 
			
		||||
		// Partner's trade address (if present)
 | 
			
		||||
		dataByteBuffer.get(addressBytes);
 | 
			
		||||
		String qortalRecipient = Base58.encode(addressBytes);
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
 | 
			
		||||
 | 
			
		||||
		// Potential lockTimeA (if in trade mode)
 | 
			
		||||
		int lockTimeA = (int) dataByteBuffer.getLong();
 | 
			
		||||
 | 
			
		||||
		// AT refund timeout (probably only useful for debugging)
 | 
			
		||||
		int refundTimeout = (int) dataByteBuffer.getLong();
 | 
			
		||||
 | 
			
		||||
		// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
 | 
			
		||||
		long tradeRefundTimestamp = dataByteBuffer.getLong();
 | 
			
		||||
 | 
			
		||||
		// Skip last transaction timestamp
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip block timestamp
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip transaction type
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip temporary result
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip temporary message sender
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
 | 
			
		||||
 | 
			
		||||
		// Skip message length
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8);
 | 
			
		||||
 | 
			
		||||
		// Skip temporary message data
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
 | 
			
		||||
 | 
			
		||||
		// Potential hash160 of secret A
 | 
			
		||||
		byte[] hashOfSecretA = new byte[20];
 | 
			
		||||
		dataByteBuffer.get(hashOfSecretA);
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
 | 
			
		||||
 | 
			
		||||
		// Potential partner's Digibyte PKH
 | 
			
		||||
		byte[] partnerDigibytePKH = new byte[20];
 | 
			
		||||
		dataByteBuffer.get(partnerDigibytePKH);
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDigibytePKH.length); // skip to 32 bytes
 | 
			
		||||
 | 
			
		||||
		// Partner's receiving address (if present)
 | 
			
		||||
		byte[] partnerReceivingAddress = new byte[25];
 | 
			
		||||
		dataByteBuffer.get(partnerReceivingAddress);
 | 
			
		||||
		dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
 | 
			
		||||
 | 
			
		||||
		// Trade AT's 'mode'
 | 
			
		||||
		long modeValue = dataByteBuffer.getLong();
 | 
			
		||||
		AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
 | 
			
		||||
 | 
			
		||||
		/* End of variables */
 | 
			
		||||
 | 
			
		||||
		if (mode != null && mode != AcctMode.OFFERING) {
 | 
			
		||||
			tradeData.mode = mode;
 | 
			
		||||
			tradeData.refundTimeout = refundTimeout;
 | 
			
		||||
			tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
 | 
			
		||||
			tradeData.qortalPartnerAddress = qortalRecipient;
 | 
			
		||||
			tradeData.hashOfSecretA = hashOfSecretA;
 | 
			
		||||
			tradeData.partnerForeignPKH = partnerDigibytePKH;
 | 
			
		||||
			tradeData.lockTimeA = lockTimeA;
 | 
			
		||||
 | 
			
		||||
			if (mode == AcctMode.REDEEMED)
 | 
			
		||||
				tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
 | 
			
		||||
		} else {
 | 
			
		||||
			tradeData.mode = AcctMode.OFFERING;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tradeData.duplicateDeprecated();
 | 
			
		||||
 | 
			
		||||
		return tradeData;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
 | 
			
		||||
	public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
 | 
			
		||||
		byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
 | 
			
		||||
		return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
 | 
			
		||||
	public static OfferMessageData extractOfferMessageData(byte[] messageData) {
 | 
			
		||||
		if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		OfferMessageData offerMessageData = new OfferMessageData();
 | 
			
		||||
		offerMessageData.partnerDigibytePKH = Arrays.copyOfRange(messageData, 0, 20);
 | 
			
		||||
		offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
 | 
			
		||||
		offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
 | 
			
		||||
 | 
			
		||||
		return offerMessageData;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
 | 
			
		||||
	public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
 | 
			
		||||
		byte[] data = new byte[TRADE_MESSAGE_LENGTH];
 | 
			
		||||
		byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
 | 
			
		||||
		byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
 | 
			
		||||
		byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
 | 
			
		||||
 | 
			
		||||
		System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
 | 
			
		||||
		System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
 | 
			
		||||
		System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
 | 
			
		||||
		System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
 | 
			
		||||
		System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
 | 
			
		||||
 | 
			
		||||
		return data;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
 | 
			
		||||
	@Override
 | 
			
		||||
	public byte[] buildCancelMessage(String creatorQortalAddress) {
 | 
			
		||||
		byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
 | 
			
		||||
		byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
 | 
			
		||||
 | 
			
		||||
		System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
 | 
			
		||||
 | 
			
		||||
		return data;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
 | 
			
		||||
	public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
 | 
			
		||||
		byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
 | 
			
		||||
		byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
 | 
			
		||||
 | 
			
		||||
		System.arraycopy(secretA, 0, data, 0, secretA.length);
 | 
			
		||||
		System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
 | 
			
		||||
 | 
			
		||||
		return data;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
 | 
			
		||||
	public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
 | 
			
		||||
		// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
 | 
			
		||||
		return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
 | 
			
		||||
		String atAddress = crossChainTradeData.qortalAtAddress;
 | 
			
		||||
		String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
 | 
			
		||||
 | 
			
		||||
		// We don't have partner's public key so we check every message to AT
 | 
			
		||||
		List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
 | 
			
		||||
		if (messageTransactionsData == null)
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		// Find 'redeem' message
 | 
			
		||||
		for (MessageTransactionData messageTransactionData : messageTransactionsData) {
 | 
			
		||||
			// Check message payload type/encryption
 | 
			
		||||
			if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			// Check message payload size
 | 
			
		||||
			byte[] messageData = messageTransactionData.getData();
 | 
			
		||||
			if (messageData.length != REDEEM_MESSAGE_LENGTH)
 | 
			
		||||
				// Wrong payload length
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			// Check sender
 | 
			
		||||
			if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
 | 
			
		||||
				// Wrong sender;
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			// Extract secretA
 | 
			
		||||
			byte[] secretA = new byte[32];
 | 
			
		||||
			System.arraycopy(messageData, 0, secretA, 0, secretA.length);
 | 
			
		||||
 | 
			
		||||
			byte[] hashOfSecretA = Crypto.hash160(secretA);
 | 
			
		||||
			if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			return secretA;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -57,6 +57,20 @@ public enum SupportedBlockchain {
 | 
			
		||||
		public ACCT getLatestAcct() {
 | 
			
		||||
			return DogecoinACCTv3.getInstance();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	DIGIBYTE(Arrays.asList(
 | 
			
		||||
			Triple.valueOf(DigibyteACCTv3.NAME, DigibyteACCTv3.CODE_BYTES_HASH, DigibyteACCTv3::getInstance)
 | 
			
		||||
		)) {
 | 
			
		||||
		@Override
 | 
			
		||||
		public ForeignBlockchain getInstance() {
 | 
			
		||||
			return Digibyte.getInstance();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public ACCT getLatestAcct() {
 | 
			
		||||
			return DigibyteACCTv3.getInstance();
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager.*;
 | 
			
		||||
import org.qortal.crosschain.Bitcoin.BitcoinNet;
 | 
			
		||||
import org.qortal.crosschain.Litecoin.LitecoinNet;
 | 
			
		||||
import org.qortal.crosschain.Dogecoin.DogecoinNet;
 | 
			
		||||
import org.qortal.crosschain.Digibyte.DigibyteNet;
 | 
			
		||||
import org.qortal.utils.EnumUtils;
 | 
			
		||||
 | 
			
		||||
// All properties to be converted to JSON via JAXB
 | 
			
		||||
@@ -222,6 +223,7 @@ public class Settings {
 | 
			
		||||
	private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
 | 
			
		||||
	private LitecoinNet litecoinNet = LitecoinNet.MAIN;
 | 
			
		||||
	private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
 | 
			
		||||
	private DigibyteNet digibyteNet = DigibyteNet.MAIN;
 | 
			
		||||
	// Also crosschain-related:
 | 
			
		||||
	/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
 | 
			
		||||
	private boolean tradebotSystrayEnabled = false;
 | 
			
		||||
@@ -680,6 +682,10 @@ public class Settings {
 | 
			
		||||
		return this.dogecoinNet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public DigibyteNet getDigibyteNet() {
 | 
			
		||||
		return this.digibyteNet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public boolean isTradebotSystrayEnabled() {
 | 
			
		||||
		return this.tradebotSystrayEnabled;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										115
									
								
								src/test/java/org/qortal/test/crosschain/DigibyteTests.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/test/java/org/qortal/test/crosschain/DigibyteTests.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
package org.qortal.test.crosschain;
 | 
			
		||||
 | 
			
		||||
import static org.junit.Assert.*;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
 | 
			
		||||
import org.bitcoinj.core.Transaction;
 | 
			
		||||
import org.bitcoinj.store.BlockStoreException;
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Ignore;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.qortal.crosschain.ForeignBlockchainException;
 | 
			
		||||
import org.qortal.crosschain.Digibyte;
 | 
			
		||||
import org.qortal.crosschain.BitcoinyHTLC;
 | 
			
		||||
import org.qortal.repository.DataException;
 | 
			
		||||
import org.qortal.test.common.Common;
 | 
			
		||||
 | 
			
		||||
public class DigibyteTests extends Common {
 | 
			
		||||
 | 
			
		||||
	private Digibyte digibyte;
 | 
			
		||||
 | 
			
		||||
	@Before
 | 
			
		||||
	public void beforeTest() throws DataException {
 | 
			
		||||
		Common.useDefaultSettings(); // TestNet3
 | 
			
		||||
		digibyte = Digibyte.getInstance();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@After
 | 
			
		||||
	public void afterTest() {
 | 
			
		||||
		Digibyte.resetForTesting();
 | 
			
		||||
		digibyte = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
 | 
			
		||||
		long before = System.currentTimeMillis();
 | 
			
		||||
		System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
 | 
			
		||||
		long afterFirst = System.currentTimeMillis();
 | 
			
		||||
 | 
			
		||||
		System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
 | 
			
		||||
		long afterSecond = System.currentTimeMillis();
 | 
			
		||||
 | 
			
		||||
		long firstPeriod = afterFirst - before;
 | 
			
		||||
		long secondPeriod = afterSecond - afterFirst;
 | 
			
		||||
 | 
			
		||||
		System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
 | 
			
		||||
 | 
			
		||||
		assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
 | 
			
		||||
		assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	@Ignore(value = "Doesn't work, to be fixed later")
 | 
			
		||||
	public void testFindHtlcSecret() throws ForeignBlockchainException {
 | 
			
		||||
		// This actually exists on TEST3 but can take a while to fetch
 | 
			
		||||
		String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
 | 
			
		||||
 | 
			
		||||
		byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
 | 
			
		||||
		byte[] secret = BitcoinyHTLC.findHtlcSecret(digibyte, p2shAddress);
 | 
			
		||||
 | 
			
		||||
		assertNotNull("secret not found", secret);
 | 
			
		||||
		assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
 | 
			
		||||
	public void testBuildSpend() {
 | 
			
		||||
		String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
 | 
			
		||||
 | 
			
		||||
		String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
 | 
			
		||||
		long amount = 1000L;
 | 
			
		||||
 | 
			
		||||
		Transaction transaction = digibyte.buildSpend(xprv58, recipient, amount);
 | 
			
		||||
		assertNotNull("insufficient funds", transaction);
 | 
			
		||||
 | 
			
		||||
		// Check spent key caching doesn't affect outcome
 | 
			
		||||
 | 
			
		||||
		transaction = digibyte.buildSpend(xprv58, recipient, amount);
 | 
			
		||||
		assertNotNull("insufficient funds", transaction);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void testGetWalletBalance() {
 | 
			
		||||
		String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
 | 
			
		||||
 | 
			
		||||
		Long balance = digibyte.getWalletBalance(xprv58);
 | 
			
		||||
 | 
			
		||||
		assertNotNull(balance);
 | 
			
		||||
 | 
			
		||||
		System.out.println(digibyte.format(balance));
 | 
			
		||||
 | 
			
		||||
		// Check spent key caching doesn't affect outcome
 | 
			
		||||
 | 
			
		||||
		Long repeatBalance = digibyte.getWalletBalance(xprv58);
 | 
			
		||||
 | 
			
		||||
		assertNotNull(repeatBalance);
 | 
			
		||||
 | 
			
		||||
		System.out.println(digibyte.format(repeatBalance));
 | 
			
		||||
 | 
			
		||||
		assertEquals(balance, repeatBalance);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
 | 
			
		||||
		String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
 | 
			
		||||
 | 
			
		||||
		String address = digibyte.getUnusedReceiveAddress(xprv58);
 | 
			
		||||
 | 
			
		||||
		assertNotNull(address);
 | 
			
		||||
 | 
			
		||||
		System.out.println(address);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,769 @@
 | 
			
		||||
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.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 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 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 Random RANDOM = new Random();
 | 
			
		||||
 | 
			
		||||
	@Before
 | 
			
		||||
	public void beforeTest() throws DataException {
 | 
			
		||||
		Common.useDefaultSettings();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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 = 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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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()));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@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");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user