forked from Qortal/qortal
Removed DigibyteACCTv1 and v2
Also removed CrossChainDigibyteACCTv1Resource, since this is unused, and it seems excessive to maintain support of this for every coin (and potentially every ACCT version).
This commit is contained in:
parent
8aed84e6af
commit
a95a37277c
@ -1,148 +0,0 @@
|
|||||||
package org.qortal.api.resource;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
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 org.qortal.account.PrivateKeyAccount;
|
|
||||||
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.CrossChainSecretRequest;
|
|
||||||
import org.qortal.crosschain.AcctMode;
|
|
||||||
import org.qortal.crosschain.DigibyteACCTv1;
|
|
||||||
import org.qortal.crypto.Crypto;
|
|
||||||
import org.qortal.data.at.ATData;
|
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
|
||||||
import org.qortal.group.Group;
|
|
||||||
import org.qortal.repository.DataException;
|
|
||||||
import org.qortal.repository.Repository;
|
|
||||||
import org.qortal.repository.RepositoryManager;
|
|
||||||
import org.qortal.transaction.MessageTransaction;
|
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
|
||||||
import org.qortal.transform.TransformationException;
|
|
||||||
import org.qortal.transform.Transformer;
|
|
||||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
|
||||||
import org.qortal.utils.Base58;
|
|
||||||
import org.qortal.utils.NTP;
|
|
||||||
|
|
||||||
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 java.util.Arrays;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Path("/crosschain/DigibyteACCTv1")
|
|
||||||
@Tag(name = "Cross-Chain (DigibyteACCTv1)")
|
|
||||||
public class CrossChainDigibyteACCTv1Resource {
|
|
||||||
|
|
||||||
@Context
|
|
||||||
HttpServletRequest request;
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("/redeemmessage")
|
|
||||||
@Operation(
|
|
||||||
summary = "Signs and broadcasts a 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
|
|
||||||
description = "Specify address of cross-chain AT that needs to be messaged, Alice's trade private key, the 32-byte secret,<br>"
|
|
||||||
+ "and an address for receiving QORT from AT. All of these can be found in Alice's trade bot data.<br>"
|
|
||||||
+ "AT needs to be in 'trade' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send!<br>"
|
|
||||||
+ "You need to use the private key that the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
|
|
||||||
requestBody = @RequestBody(
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON,
|
|
||||||
schema = @Schema(
|
|
||||||
implementation = CrossChainSecretRequest.class
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(
|
|
||||||
type = "string"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
|
||||||
@SecurityRequirement(name = "apiKey")
|
|
||||||
public boolean buildRedeemMessage(@HeaderParam(Security.API_KEY_HEADER) String apiKey, CrossChainSecretRequest secretRequest) {
|
|
||||||
Security.checkApiCallAllowed(request);
|
|
||||||
|
|
||||||
byte[] partnerPrivateKey = secretRequest.partnerPrivateKey;
|
|
||||||
|
|
||||||
if (partnerPrivateKey == null || partnerPrivateKey.length != Transformer.PRIVATE_KEY_LENGTH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
|
||||||
|
|
||||||
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
|
||||||
|
|
||||||
if (secretRequest.secret == null || secretRequest.secret.length != DigibyteACCTv1.SECRET_LENGTH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
|
|
||||||
CrossChainTradeData crossChainTradeData = DigibyteACCTv1.getInstance().populateTradeData(repository, atData);
|
|
||||||
|
|
||||||
if (crossChainTradeData.mode != AcctMode.TRADING)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
byte[] partnerPublicKey = new PrivateKeyAccount(null, partnerPrivateKey).getPublicKey();
|
|
||||||
String partnerAddress = Crypto.toAddress(partnerPublicKey);
|
|
||||||
|
|
||||||
// MESSAGE must come from address that AT considers trade partner
|
|
||||||
if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
|
||||||
|
|
||||||
// Good to make MESSAGE
|
|
||||||
|
|
||||||
byte[] messageData = DigibyteACCTv1.buildRedeemMessage(secretRequest.secret, secretRequest.receivingAddress);
|
|
||||||
|
|
||||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, partnerPrivateKey);
|
|
||||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, secretRequest.atAddress, messageData, false, false);
|
|
||||||
|
|
||||||
messageTransaction.computeNonce();
|
|
||||||
messageTransaction.sign(sender);
|
|
||||||
|
|
||||||
// reset repository state to prevent deadlock
|
|
||||||
repository.discardChanges();
|
|
||||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
|
||||||
|
|
||||||
if (result != ValidationResult.OK)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (DataException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
|
||||||
if (atData == null)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
|
||||||
|
|
||||||
// Must be correct AT - check functionality using code hash
|
|
||||||
if (!Arrays.equals(atData.getCodeHash(), DigibyteACCTv1.CODE_BYTES_HASH))
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
// No point sending message to AT that's finished
|
|
||||||
if (atData.getIsFinished())
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
return atData;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,896 +0,0 @@
|
|||||||
package org.qortal.controller.tradebot;
|
|
||||||
|
|
||||||
import static java.util.Arrays.stream;
|
|
||||||
import static java.util.stream.Collectors.toMap;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.bitcoinj.core.Address;
|
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
import org.bitcoinj.core.ECKey;
|
|
||||||
import org.bitcoinj.core.Transaction;
|
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
|
||||||
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.ACCT;
|
|
||||||
import org.qortal.crosschain.AcctMode;
|
|
||||||
import org.qortal.crosschain.ForeignBlockchainException;
|
|
||||||
import org.qortal.crosschain.Digibyte;
|
|
||||||
import org.qortal.crosschain.DigibyteACCTv1;
|
|
||||||
import org.qortal.crosschain.SupportedBlockchain;
|
|
||||||
import org.qortal.crosschain.BitcoinyHTLC;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 DigibyteACCTv1TradeBot implements AcctTradeBot {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv1TradeBot.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 DigibyteACCTv1TradeBot 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 DigibyteACCTv1TradeBot() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized DigibyteACCTv1TradeBot getInstance() {
|
|
||||||
if (instance == null)
|
|
||||||
instance = new DigibyteACCTv1TradeBot();
|
|
||||||
|
|
||||||
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 = DigibyteACCTv1.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, DigibyteACCTv1.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, DigibyteACCTv1.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 = DigibyteACCTv1.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 = DigibyteACCTv1.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();
|
|
||||||
DigibyteACCTv1.OfferMessageData offerMessageData = DigibyteACCTv1.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 = DigibyteACCTv1.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 = DigibyteACCTv1.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 = DigibyteACCTv1.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 = DigibyteACCTv1.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 = DigibyteACCTv1.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,885 +0,0 @@
|
|||||||
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 DigibyteACCTv2TradeBot implements AcctTradeBot {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv2TradeBot.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 DigibyteACCTv2TradeBot 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 DigibyteACCTv2TradeBot() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized DigibyteACCTv2TradeBot getInstance() {
|
|
||||||
if (instance == null)
|
|
||||||
instance = new DigibyteACCTv2TradeBot();
|
|
||||||
|
|
||||||
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 = DigibyteACCTv2.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, DigibyteACCTv2.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, DigibyteACCTv2.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 = DigibyteACCTv2.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 = DigibyteACCTv2.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();
|
|
||||||
DigibyteACCTv2.OfferMessageData offerMessageData = DigibyteACCTv2.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 = DigibyteACCTv2.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 = DigibyteACCTv2.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 = DigibyteACCTv2.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 = DigibyteACCTv2.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 = DigibyteACCTv2.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,8 +100,6 @@ public class TradeBot implements Listener {
|
|||||||
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(DigibyteACCTv1.class, DigibyteACCTv1TradeBot::getInstance);
|
|
||||||
acctTradeBotSuppliers.put(DigibyteACCTv2.class, DigibyteACCTv2TradeBot::getInstance);
|
|
||||||
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,861 +0,0 @@
|
|||||||
package org.qortal.crosschain;
|
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.ciyam.at.API;
|
|
||||||
import org.ciyam.at.CompilationException;
|
|
||||||
import org.ciyam.at.FunctionCode;
|
|
||||||
import org.ciyam.at.MachineState;
|
|
||||||
import org.ciyam.at.OpCode;
|
|
||||||
import org.ciyam.at.Timestamp;
|
|
||||||
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 com.google.common.hash.HashCode;
|
|
||||||
import com.google.common.primitives.Bytes;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 DigibyteACCTv1 implements ACCT {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv1.class);
|
|
||||||
|
|
||||||
public static final String NAME = DigibyteACCTv1.class.getSimpleName();
|
|
||||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("39cbdfd55ee7257dd637683a1c5a5fee920ef1a5676dc98db0461f57a9465ac8").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 DigibyteACCTv1 instance;
|
|
||||||
|
|
||||||
private DigibyteACCTv1() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized DigibyteACCTv1 getInstance() {
|
|
||||||
if (instance == null)
|
|
||||||
instance = new DigibyteACCTv1();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// 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), DigibyteACCTv1.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,861 +0,0 @@
|
|||||||
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 DigibyteACCTv2 implements ACCT {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv2.class);
|
|
||||||
|
|
||||||
public static final String NAME = DigibyteACCTv2.class.getSimpleName();
|
|
||||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("607e17eba9764a69e35cbbf5edc57f69ff75d1a17ad39da4ae1893f4857ff4c6").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 DigibyteACCTv2 instance;
|
|
||||||
|
|
||||||
private DigibyteACCTv2() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized DigibyteACCTv2 getInstance() {
|
|
||||||
if (instance == null)
|
|
||||||
instance = new DigibyteACCTv2();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
/* Sleep until message arrives */
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
|
|
||||||
|
|
||||||
// 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), DigibyteACCTv2.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -60,8 +60,6 @@ public enum SupportedBlockchain {
|
|||||||
},
|
},
|
||||||
|
|
||||||
DIGIBYTE(Arrays.asList(
|
DIGIBYTE(Arrays.asList(
|
||||||
Triple.valueOf(DigibyteACCTv1.NAME, DigibyteACCTv1.CODE_BYTES_HASH, DigibyteACCTv1::getInstance),
|
|
||||||
Triple.valueOf(DigibyteACCTv2.NAME, DigibyteACCTv2.CODE_BYTES_HASH, DigibyteACCTv2::getInstance),
|
|
||||||
Triple.valueOf(DigibyteACCTv3.NAME, DigibyteACCTv3.CODE_BYTES_HASH, DigibyteACCTv3::getInstance)
|
Triple.valueOf(DigibyteACCTv3.NAME, DigibyteACCTv3.CODE_BYTES_HASH, DigibyteACCTv3::getInstance)
|
||||||
)) {
|
)) {
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user