WIP: trade-bot: more receive address support, some terminology clarification

Bitcoin receive address no longer stored in AT but dealt with by trade-bot.
This allows 'Bob' to have his BTC sent anywhere he likes when redeeming P2SH-A
thus saving a step, typically incurred by UI. DB shape change due to this.

Similarly, AT code has been updated to expect a Qortal receiving address when
Alice sends MESSAGE to redeem AT.

This means both trade-bot entries (Alice/Bob) can be safely wiped once trade completes.

Some terms were confusing like "trade recipient" which actually referred to
Alice and so have been unified as "trade partner" as to not be confused with
(say) "recipient address"

The MESSAGEs sent from Alice to Bob, from Bob to AT and from Alice to AT have been
given more useful names: 'offer', 'trade' and 'redeem'. There is also a cancel
MESSAGE sent from Bob to AT to cancel AT before trading occurs.

Some API calls have been renamed in light of above.

AT's 'mode' has been expanded from simply OFFER/TRADE to:
OFFERING, TRADING, REFUNDED, REDEEMED, CANCELLED

Tests updated, but MORE TESTING REQUIRED BEFORE RELEASE
This commit is contained in:
catbref 2020-08-03 09:36:46 +01:00
parent 6be67d0d92
commit 876bfb525b
14 changed files with 436 additions and 400 deletions

View File

@ -33,9 +33,6 @@ public class CrossChainBuildRequest {
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080") @Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
public Integer tradeTimeout; public Integer tradeTimeout;
@Schema(description = "Bitcoin address for receiving", example = "1NCTG9oLk41bU6pcehLNo9DVJup77EHAVx")
public String receiveAddress;
public CrossChainBuildRequest() { public CrossChainBuildRequest() {
} }

View File

@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest { public class CrossChainCancelRequest {
@Schema(description = "AT's trade public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "AT creator's 'trade' public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] tradePublicKey; public byte[] tradePublicKey;
@Schema(description = "Qortal trade AT address") @Schema(description = "Qortal trade AT address")

View File

@ -8,8 +8,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest { public class CrossChainSecretRequest {
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] recipientPublicKey; public byte[] partnerPublicKey;
@Schema(description = "Qortal AT address") @Schema(description = "Qortal AT address")
public String atAddress; public String atAddress;
@ -20,6 +20,9 @@ public class CrossChainSecretRequest {
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx") @Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
public byte[] secretB; public byte[] secretB;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public CrossChainSecretRequest() { public CrossChainSecretRequest() {
} }

View File

@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeRequest { public class CrossChainTradeRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] tradePublicKey; public byte[] tradePublicKey;
@Schema(description = "Qortal AT address") @Schema(description = "Qortal AT address")
public String atAddress; public String atAddress;
@Schema(description = "Signature of trading partner's MESSAGE transaction") @Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
public byte[] messageTransactionSignature; public byte[] messageTransactionSignature;
public CrossChainTradeRequest() { public CrossChainTradeRequest() {

View File

@ -14,6 +14,9 @@ public class TradeBotRespondRequest {
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ") @Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
public String xprv58; public String xprv58;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public TradeBotRespondRequest() { public TradeBotRespondRequest() {
} }

View File

@ -59,7 +59,6 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData; import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData; import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.crosschain.CrossChainTradeData.Mode;
import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData; import org.qortal.data.transaction.MessageTransactionData;
@ -182,23 +181,11 @@ public class CrossChainResource {
if (tradeRequest.bitcoinAmount <= 0) if (tradeRequest.bitcoinAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
Address bitcoinReceiveAddress;
try {
bitcoinReceiveAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeRequest.receiveAddress);
} catch (AddressFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
}
byte[] bitcoinReceivePublicKeyHash = bitcoinReceiveAddress.getHash();
// We only support P2PKH addresses at this time
if (bitcoinReceiveAddress.getOutputScriptType() != ScriptType.P2PKH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB, byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB,
tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout, bitcoinReceivePublicKeyHash); tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout);
long txTimestamp = NTP.getTime(); long txTimestamp = NTP.getTime();
byte[] lastReference = creatorAccount.getLastReference(); byte[] lastReference = creatorAccount.getLastReference();
@ -233,11 +220,11 @@ public class CrossChainResource {
} }
@POST @POST
@Path("/tradeoffer/recipient") @Path("/tradeoffer/trademessage")
@Operation( @Operation(
summary = "Builds raw, unsigned MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode", summary = "Builds raw, unsigned 'trade' MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode",
description = "Specify address of cross-chain AT that needs to be messaged, and address of Qortal recipient.<br>" description = "Specify address of cross-chain AT that needs to be messaged, and signature of 'offer' MESSAGE from trade partner.<br>"
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>" + "AT needs to be in 'offer' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with trade private key otherwise the MESSAGE transaction will be invalid.", + "You need to sign output with trade private key otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody( requestBody = @RequestBody(
required = true, required = true,
@ -259,7 +246,7 @@ public class CrossChainResource {
} }
) )
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public String sendTradeRecipient(CrossChainTradeRequest tradeRequest) { public String buildTradeMessage(CrossChainTradeRequest tradeRequest) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
byte[] tradePublicKey = tradeRequest.tradePublicKey; byte[] tradePublicKey = tradeRequest.tradePublicKey;
@ -277,11 +264,11 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE) if (crossChainTradeData.mode != BTCACCT.Mode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match trade public key? // Does supplied public key match trade public key?
if (tradePublicKey != null && !Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress)) if (!Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
TransactionData transactionData = repository.getTransactionRepository().fromSignature(tradeRequest.messageTransactionSignature); TransactionData transactionData = repository.getTransactionRepository().fromSignature(tradeRequest.messageTransactionSignature);
@ -299,7 +286,7 @@ public class CrossChainResource {
// Good to make MESSAGE // Good to make MESSAGE
byte[] aliceForeignPublicKeyHash = offerMessageData.recipientBitcoinPKH; byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA; byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA; int lockTimeA = (int) offerMessageData.lockTimeA;
@ -316,12 +303,12 @@ public class CrossChainResource {
} }
@POST @POST
@Path("/tradeoffer/secret") @Path("/tradeoffer/redeemmessage")
@Operation( @Operation(
summary = "Builds raw, unsigned MESSAGE transaction that sends secrets to AT, releasing funds to recipient", summary = "Builds raw, unsigned 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
description = "Specify address of cross-chain AT that needs to be messaged, and both 32-byte secrets.<br>" description = "Specify address of cross-chain AT that needs to be messaged, both 32-byte secrets and an address for receiving QORT from AT.<br>"
+ "AT needs to be in 'trade' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<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 sign output with account the AT considers the 'recipient' otherwise the MESSAGE transaction will be invalid.", + "You need to sign output with account the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody( requestBody = @RequestBody(
required = true, required = true,
content = @Content( content = @Content(
@ -342,12 +329,12 @@ public class CrossChainResource {
} }
) )
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public String sendSecret(CrossChainSecretRequest secretRequest) { public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
byte[] recipientPublicKey = secretRequest.recipientPublicKey; byte[] partnerPublicKey = secretRequest.partnerPublicKey;
if (recipientPublicKey == null || recipientPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) if (partnerPublicKey == null || partnerPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress)) if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
@ -359,24 +346,26 @@ public class CrossChainResource {
if (secretRequest.secretB == null || secretRequest.secretB.length != BTCACCT.SECRET_LENGTH) if (secretRequest.secretB == null || secretRequest.secretB.length != BTCACCT.SECRET_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); 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()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER) if (crossChainTradeData.mode != BTCACCT.Mode.TRADING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, recipientPublicKey); String partnerAddress = Crypto.toAddress(partnerPublicKey);
String recipientAddress = recipientAccount.getAddress();
// MESSAGE must come from address that AT considers trade partner / 'recipient' // MESSAGE must come from address that AT considers trade partner
if (!crossChainTradeData.qortalRecipient.equals(recipientAddress)) if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// Good to make MESSAGE // Good to make MESSAGE
byte[] messageData = BTCACCT.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB); byte[] messageData = BTCACCT.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB, secretRequest.receivingAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, messageData); byte[] messageTransactionBytes = buildAtMessage(repository, partnerPublicKey, secretRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes); return Base58.encode(messageTransactionBytes);
} catch (DataException e) { } catch (DataException e) {
@ -387,7 +376,7 @@ public class CrossChainResource {
@DELETE @DELETE
@Path("/tradeoffer") @Path("/tradeoffer")
@Operation( @Operation(
summary = "Builds raw, unsigned MESSAGE transaction that cancels cross-chain trade offer", summary = "Builds raw, unsigned 'cancel' MESSAGE transaction that cancels cross-chain trade offer",
description = "Specify address of cross-chain AT that needs to be cancelled.<br>" description = "Specify address of cross-chain AT that needs to be cancelled.<br>"
+ "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>" + "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with trade's private key otherwise the MESSAGE transaction will be invalid.", + "You need to sign output with trade's private key otherwise the MESSAGE transaction will be invalid.",
@ -411,7 +400,7 @@ public class CrossChainResource {
} }
) )
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public String cancelTradeOffer(CrossChainCancelRequest cancelRequest) { public String buildCancelMessage(CrossChainCancelRequest cancelRequest) {
Security.checkApiCallAllowed(request); Security.checkApiCallAllowed(request);
byte[] tradePublicKey = cancelRequest.tradePublicKey; byte[] tradePublicKey = cancelRequest.tradePublicKey;
@ -426,17 +415,17 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE) if (crossChainTradeData.mode != BTCACCT.Mode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Does supplied public key match trade public key? // Does supplied public key match trade public key?
if (tradePublicKey != null && !Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress)) if (!Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
// Good to make MESSAGE // Good to make MESSAGE
String atCreatorAddress = crossChainTradeData.qortalCreator; String atCreatorAddress = crossChainTradeData.qortalCreator;
byte[] messageData = BTCACCT.buildRefundMessage(atCreatorAddress); byte[] messageData = BTCACCT.buildCancelMessage(atCreatorAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, cancelRequest.atAddress, messageData); byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, cancelRequest.atAddress, messageData);
@ -516,7 +505,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER) if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, lockTimeFn.applyAsInt(crossChainTradeData), templateRequest.redeemPublicKeyHash, hashOfSecretFn.apply(crossChainTradeData)); byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, lockTimeFn.applyAsInt(crossChainTradeData), templateRequest.redeemPublicKeyHash, hashOfSecretFn.apply(crossChainTradeData));
@ -599,7 +588,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER) if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
@ -732,7 +721,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER) if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
@ -874,7 +863,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, redeemRequest.atAddress); ATData atData = fetchAtDataWithChecking(repository, redeemRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == Mode.OFFER) if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING || crossChainTradeData.mode == BTCACCT.Mode.CANCELLED)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData); int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
@ -1031,15 +1020,18 @@ public class CrossChainResource {
if (!BTC.getInstance().isValidXprv(tradeBotRespondRequest.xprv58)) if (!BTC.getInstance().isValidXprv(tradeBotRespondRequest.xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// Extract data from cross-chain trading AT // Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, atAddress); ATData atData = fetchAtDataWithChecking(repository, atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode != Mode.OFFER) if (crossChainTradeData.mode != BTCACCT.Mode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
boolean result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58); boolean result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58, tradeBotRespondRequest.receivingAddress);
return result ? "true" : "false"; return result ? "true" : "false";
} catch (DataException e) { } catch (DataException e) {

View File

@ -40,6 +40,7 @@ import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException; import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer; import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Amounts; import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP; import org.qortal.utils.NTP;
public class TradeBot { public class TradeBot {
@ -133,7 +134,7 @@ public class TradeBot {
String aTType = "ACCT"; String aTType = "ACCT";
String tags = "ACCT QORT BTC"; String tags = "ACCT QORT BTC";
byte[] creationBytes = BTCACCT.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount, byte[] creationBytes = BTCACCT.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount,
tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout, bitcoinReceivePublicKeyHash); tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount; long amount = tradeBotCreateRequest.fundingQortAmount;
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT); DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
@ -150,7 +151,7 @@ public class TradeBot {
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretB, hashOfSecretB, secretB, hashOfSecretB,
tradeForeignPublicKey, tradeForeignPublicKeyHash, tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.bitcoinAmount, null, null, null); tradeBotCreateRequest.bitcoinAmount, null, null, null, bitcoinReceivePublicKeyHash);
repository.getCrossChainRepository().save(tradeBotData); repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges(); repository.saveChanges();
@ -188,7 +189,7 @@ public class TradeBot {
* <p> * <p>
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet. * It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
* <p> * <p>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A<b> * If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
* with the Bitcoin amount expected by 'Bob'. * with the Bitcoin amount expected by 'Bob'.
* <p> * <p>
* If the Bitcoin transaction is successfully broadcast to the network then the trade-bot entry * If the Bitcoin transaction is successfully broadcast to the network then the trade-bot entry
@ -202,7 +203,7 @@ public class TradeBot {
* @return true if P2SH-A funding transaction successfully broadcast to Bitcoin network, false otherwise * @return true if P2SH-A funding transaction successfully broadcast to Bitcoin network, false otherwise
* @throws DataException * @throws DataException
*/ */
public static boolean startResponse(Repository repository, CrossChainTradeData crossChainTradeData, String xprv58) throws DataException { public static boolean startResponse(Repository repository, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
byte[] tradePrivateKey = generateTradePrivateKey(); byte[] tradePrivateKey = generateTradePrivateKey();
byte[] secretA = generateSecret(); byte[] secretA = generateSecret();
byte[] hashOfSecretA = Crypto.hash160(secretA); byte[] hashOfSecretA = Crypto.hash160(secretA);
@ -213,6 +214,7 @@ public class TradeBot {
byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey); byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
// We need to generate lockTime-A: halfway of refundTimeout from now // We need to generate lockTime-A: halfway of refundTimeout from now
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L); int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L);
@ -222,7 +224,7 @@ public class TradeBot {
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretA, hashOfSecretA, secretA, hashOfSecretA,
tradeForeignPublicKey, tradeForeignPublicKeyHash, tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA); crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA, receivingPublicKeyHash);
// Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin // Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin
String tradeForeignAddress = BTC.getInstance().pkhToAddress(tradeForeignPublicKeyHash); String tradeForeignAddress = BTC.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
@ -499,7 +501,7 @@ public class TradeBot {
if (offerMessageData == null) if (offerMessageData == null)
continue; continue;
byte[] aliceForeignPublicKeyHash = offerMessageData.recipientBitcoinPKH; byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA; byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA; int lockTimeA = (int) offerMessageData.lockTimeA;
// Determine P2SH-A address and confirm funded // Determine P2SH-A address and confirm funded
@ -592,11 +594,11 @@ public class TradeBot {
} }
// We're waiting for AT to be in TRADE mode // We're waiting for AT to be in TRADE mode
if (crossChainTradeData.mode != CrossChainTradeData.Mode.TRADE) if (crossChainTradeData.mode != BTCACCT.Mode.TRADING)
return; return;
// We're expecting AT to be locked to our native trade address // We're expecting AT to be locked to our native trade address
if (!crossChainTradeData.qortalRecipient.equals(tradeBotData.getTradeNativeAddress())) { if (!crossChainTradeData.qortalPartnerAddress.equals(tradeBotData.getTradeNativeAddress())) {
// AT locked to different address! We shouldn't continue but wait and refund. // AT locked to different address! We shouldn't continue but wait and refund.
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
@ -604,7 +606,7 @@ public class TradeBot {
LOGGER.warn(() -> String.format("AT %s locked to %s, not us (%s). Refunding %s - aborting trade", LOGGER.warn(() -> String.format("AT %s locked to %s, not us (%s). Refunding %s - aborting trade",
tradeBotData.getAtAddress(), tradeBotData.getAtAddress(),
crossChainTradeData.qortalRecipient, crossChainTradeData.qortalPartnerAddress,
tradeBotData.getTradeNativeAddress(), tradeBotData.getTradeNativeAddress(),
p2shAddress)); p2shAddress));
@ -701,7 +703,7 @@ public class TradeBot {
// AT yet to process MESSAGE // AT yet to process MESSAGE
return; return;
byte[] redeemScriptBytes = BTCP2SH.buildScript(crossChainTradeData.recipientBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB); byte[] redeemScriptBytes = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Long balance = BTC.getInstance().getBalance(p2shAddress); Long balance = BTC.getInstance().getBalance(p2shAddress);
@ -716,7 +718,7 @@ public class TradeBot {
Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress); List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
byte[] receivePublicKeyHash = crossChainTradeData.creatorReceiveBitcoinPKH; byte[] receivePublicKeyHash = tradeBotData.getReceivingPublicKeyHash();
Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, tradeBotData.getSecret(), receivePublicKeyHash); Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, tradeBotData.getSecret(), receivePublicKeyHash);
@ -783,9 +785,10 @@ public class TradeBot {
// Secret not revealed at this time // Secret not revealed at this time
return; return;
// Send MESSAGE to AT using both secrets // Send 'redeem' MESSAGE to AT using both secrets
byte[] secretA = tradeBotData.getSecret(); byte[] secretA = tradeBotData.getSecret();
byte[] messageData = BTCACCT.buildRedeemMessage(secretA, secretB); String qortalReceiveAddress = Base58.encode(tradeBotData.getReceivingPublicKeyHash()); // Actually contains whole address, not just PKH
byte[] messageData = BTCACCT.buildRedeemMessage(secretA, secretB, qortalReceiveAddress);
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), messageData, false, false); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), messageData, false, false);
@ -846,7 +849,7 @@ public class TradeBot {
} }
// We check variable in AT that is set when trade successfully completes // We check variable in AT that is set when trade successfully completes
if (!crossChainTradeData.hasRedeemed) { if (crossChainTradeData.mode != BTCACCT.Mode.REDEEMED) {
tradeBotData.setState(TradeBotData.State.BOB_REFUNDED); tradeBotData.setState(TradeBotData.State.BOB_REFUNDED);
repository.getCrossChainRepository().save(tradeBotData); repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges(); repository.saveChanges();
@ -864,13 +867,13 @@ public class TradeBot {
// Use secret-A to redeem P2SH-A // Use secret-A to redeem P2SH-A
byte[] redeemScriptBytes = BTCP2SH.buildScript(crossChainTradeData.recipientBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA); byte[] redeemScriptBytes = BTCP2SH.buildScript(crossChainTradeData.partnerBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin); Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress); List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
byte[] receivePublicKeyHash = crossChainTradeData.creatorReceiveBitcoinPKH; byte[] receivePublicKeyHash = tradeBotData.getReceivingPublicKeyHash();
Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA, receivePublicKeyHash); Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA, receivePublicKeyHash);

View File

@ -1,10 +1,13 @@
package org.qortal.crosschain; package org.qortal.crosschain;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import static org.ciyam.at.OpCode.calcOffset; import static org.ciyam.at.OpCode.calcOffset;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import org.ciyam.at.API; import org.ciyam.at.API;
import org.ciyam.at.CompilationException; import org.ciyam.at.CompilationException;
@ -37,7 +40,7 @@ import com.google.common.primitives.Bytes;
* <li>Bob generates Bitcoin & Qortal 'trade' keys, and secret-b * <li>Bob generates Bitcoin & Qortal 'trade' keys, and secret-b
* <ul> * <ul>
* <li>private key required to sign P2SH redeem tx</li> * <li>private key required to sign P2SH redeem tx</li>
* <li>private key can be used to create 'secret' (e.g. double-SHA256)</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> * <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
* </ul> * </ul>
* </li> * </li>
@ -48,42 +51,53 @@ import com.google.common.primitives.Bytes;
* <li>Alice finds Qortal AT and wants to trade * <li>Alice finds Qortal AT and wants to trade
* <ul> * <ul>
* <li>Alice generates Bitcoin & Qortal 'trade' keys</li> * <li>Alice generates Bitcoin & Qortal 'trade' keys</li>
* <li>Alice funds Bitcoin P2SH-a</li> * <li>Alice funds Bitcoin P2SH-A</li>
* <li>Alice MESSAGEs Bob from her Qortal trade address, sending secret-hash-a and Bitcoin PKH</li> * <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
* <ul>
* <li>hash-of-secret-A</li>
* <li>her 'trade' Bitcoin PKH</li>
* </ul>
* </li>
* </ul> * </ul>
* </li> * </li>
* <li>Bob receives MESSAGE * <li>Bob receives "offer" MESSAGE
* <ul> * <ul>
* <li>Checks Alice's P2SH-a</li> * <li>Checks Alice's P2SH-A</li>
* <li>Sends MESSAGE to Qortal AT from his trade address, containing: * <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
* <ul> * <ul>
* <li>Alice's trade Qortal address</li> * <li>Alice's trade Qortal address</li>
* <li>Alice's trade Bitcoin PKH</li> * <li>Alice's trade Bitcoin PKH</li>
* <li>secret-hash-a</li> * <li>hash-of-secret-A</li>
* </ul> * </ul>
* </li> * </li>
* </ul> * </ul>
* </li> * </li>
* <li>Alice checks Qortal AT to confirm it's locked to her * <li>Alice checks Qortal AT to confirm it's locked to her
* <ul> * <ul>
* <li>Alice creates/funds Bitcoin P2SH-b</li> * <li>Alice creates/funds Bitcoin P2SH-B</li>
* </ul> * </ul>
* </li> * </li>
* <li>Bob checks P2SH-b is funded * <li>Bob checks P2SH-B is funded
* <ul> * <ul>
* <li>Bob redeems P2SH-b using his Bitcoin trade key and secret-b</li> * <li>Bob redeems P2SH-B using his Bitcoin trade key and secret-B</li>
* </ul> * </ul>
* </li> * </li>
* <li>Alice scans P2SH-b redeem tx to extract secret-b * <li>Alice scans P2SH-B redeem transaction to extract secret-B
* <ul> * <ul>
* <li>Alice MESSAGEs Qortal AT from her trade address, sending secret-a & secret-b</li> * <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
* <li>AT's QORT funds end up at Qortal address derived from Alice's trade private key</li> * <ul>
* <li>secret-A</li>
* <li>secret-B</li>
* <li>Qortal receive address of her chosing</li>
* </ul>
* </li>
* <li>AT's QORT funds are sent to Qortal receive address</li>
* </ul> * </ul>
* </li> * </li>
* <li>Bob checks AT, extracts secret-a * <li>Bob checks AT, extracts secret-A
* <ul> * <ul>
* <li>Bob redeems P2SH-a using his Bitcoin trade key and secret-a</li> * <li>Bob redeems P2SH-A using his Bitcoin trade key and secret-A</li>
* <li>P2SH-a funds end up in at Bitcoin address derived from Bob's trade private key</li> * <li>P2SH-A BTC funds end up at Bitcoin address determined by redeem transaction output(s)</li>
* </ul> * </ul>
* </li> * </li>
* </ul> * </ul>
@ -92,13 +106,36 @@ public class BTCACCT {
public static final int SECRET_LENGTH = 32; public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000; public static final int MIN_LOCKTIME = 1500000000;
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("f62e1447e8361703c261c8e8e1973713d29b713716582f491431cb7f6ee99e80").asBytes(); // SHA256 of AT code bytes public static final byte[] CODE_BYTES_HASH = HashCode.fromString("fad14381b77ae1a2bfe7e16a1a8b571839c5f405fca0490ead08499ac170f65b").asBytes(); // SHA256 of AT code bytes
public static class OfferMessageData { public static class OfferMessageData {
public byte[] recipientBitcoinPKH; public byte[] partnerBitcoinPKH;
public byte[] hashOfSecretA; public byte[] hashOfSecretA;
public long lockTimeA; public long lockTimeA;
} }
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerBitcoinPKH*/ + 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 Bitcoin PKH (padded from 20 to 24)*/
+ 8 /*lockTimeB*/
+ 24 /*hash of secret-A (padded from 20 to 24)*/
+ 8 /*lockTimeA*/;
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret*/ + 32 /*secret*/ + 32 /*partner's Qortal receive address padded from 25 to 32*/;
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
public enum Mode {
OFFERING(0), TRADING(1), CANCELLED(2), REFUNDED(3), REDEEMED(4);
public final int value;
private static final Map<Integer, Mode> map = stream(Mode.values()).collect(toMap(mode -> mode.value, mode -> mode));
Mode(int value) {
this.value = value;
}
public static Mode valueOf(int value) {
return map.get(value);
}
}
private BTCACCT() { private BTCACCT() {
} }
@ -106,7 +143,7 @@ public class BTCACCT {
/** /**
* Returns Qortal AT creation bytes for cross-chain trading AT. * Returns Qortal AT creation bytes for cross-chain trading AT.
* <p> * <p>
* <tt>tradeTimeout</tt> (minutes) is the time window for the recipient to send the * <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. * 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
* *
* @param creatorTradeAddress AT creator's trade Qortal address, also used for refunds * @param creatorTradeAddress AT creator's trade Qortal address, also used for refunds
@ -117,7 +154,7 @@ public class BTCACCT {
* @param tradeTimeout suggested timeout for entire trade * @param tradeTimeout suggested timeout for entire trade
* @return * @return
*/ */
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout, byte[] bitcoinReceivePublicKeyHash) { public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) {
// Labels for data segment addresses // Labels for data segment addresses
int addrCounter = 0; int addrCounter = 0;
@ -138,28 +175,26 @@ public class BTCACCT {
final int addrBitcoinAmount = addrCounter++; final int addrBitcoinAmount = addrCounter++;
final int addrTradeTimeout = addrCounter++; final int addrTradeTimeout = addrCounter++;
final int addrMessageTxType = addrCounter++; final int addrMessageTxnType = addrCounter++;
final int addrExpectedOfferMessageLength = addrCounter++;
final int addrExpectedTradeMessageLength = addrCounter++; final int addrExpectedTradeMessageLength = addrCounter++;
final int addrExpectedRedeemMessageLength = addrCounter++;
final int addrCreatorAddressPointer = addrCounter++; final int addrCreatorAddressPointer = addrCounter++;
final int addrHashOfSecretBPointer = addrCounter++; final int addrHashOfSecretBPointer = addrCounter++;
final int addrQortalRecipientPointer = addrCounter++; final int addrQortalPartnerAddressPointer = addrCounter++;
final int addrMessageSenderPointer = addrCounter++; final int addrMessageSenderPointer = addrCounter++;
final int addrOfferMessageRecipientBitcoinPKHOffset = addrCounter++; final int addrTradeMessagePartnerBitcoinPKHOffset = addrCounter++;
final int addrRecipientBitcoinPKHPointer = addrCounter++; final int addrPartnerBitcoinPKHPointer = addrCounter++;
final int addrOfferMessageHashOfSecretAOffset = addrCounter++; final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
final int addrHashOfSecretAPointer = addrCounter++; final int addrHashOfSecretAPointer = addrCounter++;
final int addrTradeMessageSecretBOffset = addrCounter++; final int addrRedeemMessageSecretBOffset = addrCounter++;
final int addrRedeemMessageReceiveAddressOffset = addrCounter++;
final int addrMessageDataPointer = addrCounter++; final int addrMessageDataPointer = addrCounter++;
final int addrMessageDataLength = addrCounter++; final int addrMessageDataLength = addrCounter++;
final int addrBitcoinReceivePublicKeyHash = addrCounter;
addrCounter += 4;
final int addrEndOfConstants = addrCounter; final int addrEndOfConstants = addrCounter;
// Variables // Variables
@ -169,18 +204,18 @@ public class BTCACCT {
final int addrCreatorAddress3 = addrCounter++; final int addrCreatorAddress3 = addrCounter++;
final int addrCreatorAddress4 = addrCounter++; final int addrCreatorAddress4 = addrCounter++;
final int addrQortalRecipient1 = addrCounter++; final int addrQortalPartnerAddress1 = addrCounter++;
final int addrQortalRecipient2 = addrCounter++; final int addrQortalPartnerAddress2 = addrCounter++;
final int addrQortalRecipient3 = addrCounter++; final int addrQortalPartnerAddress3 = addrCounter++;
final int addrQortalRecipient4 = addrCounter++; final int addrQortalPartnerAddress4 = addrCounter++;
final int addrLockTimeA = addrCounter++; final int addrLockTimeA = addrCounter++;
final int addrLockTimeB = addrCounter++; final int addrLockTimeB = addrCounter++;
final int addrRefundTimeout = addrCounter++; final int addrRefundTimeout = addrCounter++;
final int addrRefundTimestamp = addrCounter++; final int addrRefundTimestamp = addrCounter++;
final int addrLastTxTimestamp = addrCounter++; final int addrLastTxnTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++; final int addrBlockTimestamp = addrCounter++;
final int addrTxType = addrCounter++; final int addrTxnType = addrCounter++;
final int addrResult = addrCounter++; final int addrResult = addrCounter++;
final int addrMessageSender1 = addrCounter++; final int addrMessageSender1 = addrCounter++;
@ -196,13 +231,11 @@ public class BTCACCT {
final int addrHashOfSecretA = addrCounter; final int addrHashOfSecretA = addrCounter;
addrCounter += 4; addrCounter += 4;
final int addrRecipientBitcoinPKH = addrCounter; final int addrPartnerBitcoinPKH = addrCounter;
addrCounter += 4; addrCounter += 4;
final int addrMode = addrCounter++; final int addrMode = addrCounter++;
final int addrRedeemFlag = addrCounter++;
// Data segment // Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
@ -232,16 +265,16 @@ public class BTCACCT {
dataByteBuffer.putLong(tradeTimeout); dataByteBuffer.putLong(tradeTimeout);
// We're only interested in MESSAGE transactions // We're only interested in MESSAGE transactions
assert dataByteBuffer.position() == addrMessageTxType * MachineState.VALUE_SIZE : "addrMessageTxType incorrect"; assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value); dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
// Expected length of OFFER MESSAGE data from AT creator // Expected length of 'trade' MESSAGE data from AT creator
assert dataByteBuffer.position() == addrExpectedOfferMessageLength * MachineState.VALUE_SIZE : "addrExpectedOfferMessageLength incorrect";
dataByteBuffer.putLong(32L + 32L + 32L);
// Expected length of TRADE MESSAGE data from trade partner / "recipient"
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect"; assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
dataByteBuffer.putLong(32L + 32L); 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 // Index into data segment of AT creator's address, used by GET_B_IND
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect"; assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
@ -252,56 +285,56 @@ public class BTCACCT {
dataByteBuffer.putLong(addrHashOfSecretB); dataByteBuffer.putLong(addrHashOfSecretB);
// Index into data segment of recipient address, used by SET_B_IND // Index into data segment of recipient address, used by SET_B_IND
assert dataByteBuffer.position() == addrQortalRecipientPointer * MachineState.VALUE_SIZE : "addrQortalRecipientPointer incorrect"; assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
dataByteBuffer.putLong(addrQortalRecipient1); dataByteBuffer.putLong(addrQortalPartnerAddress1);
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND // 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"; assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
dataByteBuffer.putLong(addrMessageSender1); dataByteBuffer.putLong(addrMessageSender1);
// Offset into OFFER MESSAGE data payload for extracting recipient's Bitcoin PKH // Offset into 'trade' MESSAGE data payload for extracting partner's Bitcoin PKH
assert dataByteBuffer.position() == addrOfferMessageRecipientBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrOfferMessageRecipientBitcoinPKHOffset incorrect"; assert dataByteBuffer.position() == addrTradeMessagePartnerBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerBitcoinPKHOffset incorrect";
dataByteBuffer.putLong(32L); dataByteBuffer.putLong(32L);
// Index into data segment of recipient's Bitcoin PKH, used by GET_B_IND // Index into data segment of partner's Bitcoin PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrRecipientBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrRecipientBitcoinPKHPointer incorrect"; assert dataByteBuffer.position() == addrPartnerBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerBitcoinPKHPointer incorrect";
dataByteBuffer.putLong(addrRecipientBitcoinPKH); dataByteBuffer.putLong(addrPartnerBitcoinPKH);
// Offset into OFFER MESSAGE data payload for extracting hash-of-secret-A // Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
assert dataByteBuffer.position() == addrOfferMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrOfferMessageHashOfSecretAOffset incorrect"; assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
dataByteBuffer.putLong(64L); dataByteBuffer.putLong(64L);
// Index into data segment of hash of secret A, used by GET_B_IND // Index into data segment to hash of secret A, used by GET_B_IND
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect"; assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
dataByteBuffer.putLong(addrHashOfSecretA); dataByteBuffer.putLong(addrHashOfSecretA);
// Offset into TRADE MESSAGE data payload for extracting secret-B // Offset into 'redeem' MESSAGE data payload for extracting secret-B
assert dataByteBuffer.position() == addrTradeMessageSecretBOffset * MachineState.VALUE_SIZE : "addrTradeMessageSecretBOffset incorrect"; assert dataByteBuffer.position() == addrRedeemMessageSecretBOffset * MachineState.VALUE_SIZE : "addrRedeemMessageSecretBOffset incorrect";
dataByteBuffer.putLong(32L); dataByteBuffer.putLong(32L);
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receive address
assert dataByteBuffer.position() == addrRedeemMessageReceiveAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceiveAddressOffset incorrect";
dataByteBuffer.putLong(64L);
// Source location and length for hashing any passed secret // Source location and length for hashing any passed secret
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect"; assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
dataByteBuffer.putLong(addrMessageData); dataByteBuffer.putLong(addrMessageData);
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect"; assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
dataByteBuffer.putLong(32L); dataByteBuffer.putLong(32L);
// Bitcoin receive public key hash
assert dataByteBuffer.position() == addrBitcoinReceivePublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinReceivePublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(bitcoinReceivePublicKeyHash, 32, 0));
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants"; assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
// Code labels // Code labels
Integer labelRefund = null; Integer labelRefund = null;
Integer labelOfferTxLoop = null; Integer labelTradeTxnLoop = null;
Integer labelCheckOfferTx = null; Integer labelCheckTradeTxn = null;
Integer labelCheckNonRefundOfferTx = null; Integer labelCheckNonRefundTradeTxn = null;
Integer labelOfferTxExtract = null; Integer labelTradeTxnExtract = null;
Integer labelTradeTxLoop = null; Integer labelRedeemTxnLoop = null;
Integer labelCheckTradeTx = null; Integer labelCheckRedeemTxn = null;
Integer labelCheckTradeSender = null; Integer labelCheckRedeemTxnSender = null;
Integer labelCheckSecretB = null; Integer labelCheckSecretB = null;
Integer labelPayout = null; Integer labelPayout = null;
@ -315,7 +348,7 @@ public class BTCACCT {
/* Initialization */ /* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT // Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp)); codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4 // 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.compile(FunctionCode.PUT_CREATOR_INTO_B));
@ -327,26 +360,26 @@ public class BTCACCT {
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */ /* 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 */ /* Transaction processing loop */
labelOfferTxLoop = codeByteBuffer.position(); labelTradeTxnLoop = codeByteBuffer.position();
// Find next transaction to this AT since the last one (if any) // 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, addrLastTxTimestamp)); 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. // 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)); 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 // 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, labelCheckOfferTx))); codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
// Stop and wait for next block // Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile()); codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */ /* Check transaction */
labelCheckOfferTx = codeByteBuffer.position(); labelCheckTradeTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction // 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, addrLastTxTimestamp)); 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 addrTxType // 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, addrTxType)); 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 // If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelOfferTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
/* Check transaction's sender. We're expecting AT creator's trade address. */ /* Check transaction's sender. We're expecting AT creator's trade address. */
@ -355,45 +388,50 @@ public class BTCACCT {
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer) // 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)); 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. // 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, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelOfferTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelOfferTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelOfferTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelOfferTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
/* Extract trade partner info from message */ /* Extract trade partner info from message */
// Extract message from transaction into B register // Extract message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B)); codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrQortalRecipient1 (as pointed to by addrQortalRecipientPointer) // 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, addrQortalRecipientPointer)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
// Compare each of recipient address with creator's address (for offer-cancel scenario). If they don't match, assume recipient is trade partner. // Compare each of partner address with creator's address (for offer-cancel scenario). If they don't match, assume address is trade partner.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelCheckNonRefundOfferTx))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalPartnerAddress1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelCheckNonRefundTradeTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelCheckNonRefundOfferTx))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalPartnerAddress2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelCheckNonRefundTradeTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelCheckNonRefundOfferTx))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalPartnerAddress3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelCheckNonRefundTradeTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelCheckNonRefundOfferTx))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalPartnerAddress4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelCheckNonRefundTradeTxn)));
// Recipient address is AT creator's address, so cancel offer and finish. // Partner address is AT creator's address, so cancel offer and finish.
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund)); codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.CANCELLED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
/* Possible switch-to-trade-mode message */ /* Possible switch-to-trade-mode message */
labelCheckNonRefundOfferTx = codeByteBuffer.position(); labelCheckNonRefundTradeTxn = codeByteBuffer.position();
// Not off-cancel scenario so check we received expected number of message bytes // Not offer-cancel scenario so check we received expected number of message bytes
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength)); codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedOfferMessageLength, calcOffset(codeByteBuffer, labelOfferTxExtract))); // If message length matches, branch to info extraction code
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelOfferTxLoop == null ? 0 : labelOfferTxLoop)); 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));
labelOfferTxExtract = codeByteBuffer.position(); /* Extracting info from 'trade' MESSAGE transaction */
labelTradeTxnExtract = codeByteBuffer.position();
// Message is expected length, grab next 32 bytes // Message is expected length, grab next 32 bytes
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOfferMessageRecipientBitcoinPKHOffset)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerBitcoinPKHOffset));
// Extract recipient's Bitcoin PKH (we only really use values from B1-B3) // Extract partner's Bitcoin PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrRecipientBitcoinPKHPointer)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerBitcoinPKHPointer));
// Also extract lockTimeB // Also extract lockTimeB
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeB)); codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeB));
// Grab next 32 bytes // Grab next 32 bytes
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOfferMessageHashOfSecretAOffset)); 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) // 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)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
@ -404,66 +442,67 @@ public class BTCACCT {
codeByteBuffer.put(OpCode.SET_DAT.compile(addrRefundTimeout, addrLockTimeA)); // refundTimeout = lockTimeA codeByteBuffer.put(OpCode.SET_DAT.compile(addrRefundTimeout, addrLockTimeA)); // refundTimeout = lockTimeA
codeByteBuffer.put(OpCode.SUB_DAT.compile(addrRefundTimeout, addrLockTimeB)); // refundTimeout -= lockTimeB codeByteBuffer.put(OpCode.SUB_DAT.compile(addrRefundTimeout, addrLockTimeB)); // refundTimeout -= lockTimeB
codeByteBuffer.put(OpCode.DIV_VAL.compile(addrRefundTimeout, 2L * 60L)); // refundTimeout /= 2 * 60 codeByteBuffer.put(OpCode.DIV_VAL.compile(addrRefundTimeout, 2L * 60L)); // refundTimeout /= 2 * 60
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this tx 'timestamp', then save into addrRefundTimestamp // 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, addrLastTxTimestamp, addrRefundTimeout)); codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
/* We are in 'trade mode' */ /* We are in 'trade mode' */
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, 1)); codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.TRADING.value));
// Set restart position to after this opcode // Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile()); codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for trade timeout or message from Qortal trade recipient containing secret-a and secret-b */ /* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
// Fetch current block 'timestamp' // Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp)); codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
// If we're not past refund 'timestamp' then look for next transaction // If we're not past refund 'timestamp' then look for next transaction
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelTradeTxLoop))); codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
// We're past refund 'timestamp' so go refund everything back to AT creator // We're past refund 'timestamp' so go refund everything back to AT creator
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund)); codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
/* Transaction processing loop */ /* Transaction processing loop */
labelTradeTxLoop = codeByteBuffer.position(); labelRedeemTxnLoop = codeByteBuffer.position();
// Find next transaction to this AT since the last one (if any) // 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, addrLastTxTimestamp)); 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. // 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)); 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 // 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, labelCheckTradeTx))); codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
// Stop and wait for next block // Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile()); codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */ /* Check transaction */
labelCheckTradeTx = codeByteBuffer.position(); labelCheckRedeemTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction // 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, addrLastTxTimestamp)); 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 addrTxType // 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, addrTxType)); 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 // If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelTradeTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check message payload length */ /* Check message payload length */
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength)); codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelCheckTradeSender))); // If message length matches, branch to sender checking code
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelOfferTxLoop == null ? 0 : labelOfferTxLoop)); 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 */ /* Check transaction's sender */
labelCheckRedeemTxnSender = codeByteBuffer.position();
labelCheckTradeSender = codeByteBuffer.position();
// Extract sender address from transaction into B register // Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B)); 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) // 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)); 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. // 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, addrQortalRecipient1, calcOffset(codeByteBuffer, labelTradeTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalRecipient2, calcOffset(codeByteBuffer, labelTradeTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalRecipient3, calcOffset(codeByteBuffer, labelTradeTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalRecipient4, calcOffset(codeByteBuffer, labelTradeTxLoop))); codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check 'secret-a' in transaction's message */ /* Check 'secret-A' in transaction's message */
// Extract secret-A from first 32 bytes of message from transaction into B register // 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)); codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
@ -476,14 +515,14 @@ public class BTCACCT {
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength)); 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 // If hashes don't match, addrResult will be zero so go find another transaction
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckSecretB))); codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckSecretB)));
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxLoop == null ? 0 : labelTradeTxLoop)); codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Check 'secret-b' in transaction's message */ /* Check 'secret-B' in transaction's message */
labelCheckSecretB = codeByteBuffer.position(); labelCheckSecretB = codeByteBuffer.position();
// Extract secret-B from next 32 bytes of message from transaction into B register // Extract secret-B 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, addrTradeMessageSecretBOffset)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageSecretBOffset));
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer) // 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)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
// Load B register with expected hash result (as pointed to by addrHashOfSecretBPointer) // Load B register with expected hash result (as pointed to by addrHashOfSecretBPointer)
@ -493,28 +532,28 @@ public class BTCACCT {
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength)); 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 // 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.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxLoop == null ? 0 : labelTradeTxLoop)); codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Success! Pay arranged amount to intended recipient */ /* Success! Pay arranged amount to receive address */
labelPayout = codeByteBuffer.position(); labelPayout = codeByteBuffer.position();
// Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer) // Extract Qortal receive address from next 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceiveAddressOffset));
// Pay AT's balance to recipient // Pay AT's balance to recipient
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
// Set redeem flag // Set redeemed mode
codeByteBuffer.put(OpCode.INC_DAT.compile(addrRedeemFlag)); codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.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 // Fall-through to refunding any remaining balance back to AT creator
/* Refund balance back to AT creator */ /* Refund balance back to AT creator */
labelRefund = codeByteBuffer.position(); labelRefund = codeByteBuffer.position();
// Load B register with AT creator's address. // Set refunded mode
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B)); codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.REFUNDED.value));
// Pay AT's balance back to AT's creator. // We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B));
// We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.compile()); codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) { } catch (CompilationException e) {
throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e); throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e);
@ -587,15 +626,16 @@ public class BTCACCT {
// Expected BTC amount // Expected BTC amount
tradeData.expectedBitcoin = dataByteBuffer.getLong(); tradeData.expectedBitcoin = dataByteBuffer.getLong();
// Trade timeout
tradeData.tradeTimeout = (int) dataByteBuffer.getLong(); tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
// Skip MESSAGE transaction type // Skip MESSAGE transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected OFFER message length // Skip expected 'trade' message length
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected TRADE message length // Skip expected 'redeem' message length
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to creator's address // Skip pointer to creator's address
@ -604,25 +644,28 @@ public class BTCACCT {
// Skip pointer to hash-of-secret-B // Skip pointer to hash-of-secret-B
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to Qortal recipient // Skip pointer to partner's Qortal trade address
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message sender // Skip pointer to message sender
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip OFFER message data offset for recipient's bitcoin PKH // Skip 'trade' message data offset for recipient's bitcoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to recipient's bitcoin PKH // Skip pointer to partner's bitcoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip OFFER message data offset for hash-of-secret-A // Skip 'trade' message data offset for hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to hash-of-secret-A // Skip pointer to hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip TRADE message data offset for secret-B // Skip 'redeem' message data offset for secret-B
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'redeem' message data offset for partner's Qortal receive address
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message data // Skip pointer to message data
@ -631,17 +674,12 @@ public class BTCACCT {
// Skip message data length // Skip message data length
dataByteBuffer.position(dataByteBuffer.position() + 8); dataByteBuffer.position(dataByteBuffer.position() + 8);
// Creator's Bitcoin/foreign receiving public key hash
tradeData.creatorReceiveBitcoinPKH = new byte[20];
dataByteBuffer.get(tradeData.creatorReceiveBitcoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorReceiveBitcoinPKH.length); // skip to 32 bytes
/* End of constants / begin variables */ /* End of constants / begin variables */
// Skip AT creator's address // Skip AT creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Recipient's trade address (if present) // Partner's trade address (if present)
dataByteBuffer.get(addressBytes); dataByteBuffer.get(addressBytes);
String qortalRecipient = Base58.encode(addressBytes); String qortalRecipient = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length); dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
@ -655,7 +693,7 @@ public class BTCACCT {
// AT refund timeout (probably only useful for debugging) // AT refund timeout (probably only useful for debugging)
int refundTimeout = (int) dataByteBuffer.getLong(); int refundTimeout = (int) dataByteBuffer.getLong();
// Trade offer timeout (AT 'timestamp' converted to Qortal block height) // Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
long tradeRefundTimestamp = dataByteBuffer.getLong(); long tradeRefundTimestamp = dataByteBuffer.getLong();
// Skip last transaction timestamp // Skip last transaction timestamp
@ -684,60 +722,58 @@ public class BTCACCT {
dataByteBuffer.get(hashOfSecretA); dataByteBuffer.get(hashOfSecretA);
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
// Potential recipient's Bitcoin PKH // Potential partner's Bitcoin PKH
byte[] recipientBitcoinPKH = new byte[20]; byte[] recipientBitcoinPKH = new byte[20];
dataByteBuffer.get(recipientBitcoinPKH); dataByteBuffer.get(recipientBitcoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - recipientBitcoinPKH.length); // skip to 32 bytes dataByteBuffer.position(dataByteBuffer.position() + 32 - recipientBitcoinPKH.length); // skip to 32 bytes
long mode = dataByteBuffer.getLong(); long modeValue = dataByteBuffer.getLong();
Mode mode = Mode.valueOf((int) (modeValue & 0xffL));
long redeemFlag = dataByteBuffer.getLong(); if (mode != null && mode != Mode.OFFERING) {
tradeData.mode = mode;
if (mode != 0) {
tradeData.mode = CrossChainTradeData.Mode.TRADE;
tradeData.refundTimeout = refundTimeout; tradeData.refundTimeout = refundTimeout;
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
tradeData.qortalRecipient = qortalRecipient; tradeData.qortalPartnerAddress = qortalRecipient;
tradeData.hashOfSecretA = hashOfSecretA; tradeData.hashOfSecretA = hashOfSecretA;
tradeData.recipientBitcoinPKH = recipientBitcoinPKH; tradeData.partnerBitcoinPKH = recipientBitcoinPKH;
tradeData.lockTimeA = lockTimeA; tradeData.lockTimeA = lockTimeA;
tradeData.lockTimeB = lockTimeB; tradeData.lockTimeB = lockTimeB;
tradeData.hasRedeemed = redeemFlag != 0;
} else { } else {
tradeData.mode = CrossChainTradeData.Mode.OFFER; tradeData.mode = Mode.OFFERING;
} }
return tradeData; return tradeData;
} }
/** Returns trade-info MESSAGE payload for trade partner/recipient to send to AT creator's trade address. */ /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
public static byte[] buildOfferMessage(byte[] recipientBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) { public static byte[] buildOfferMessage(byte[] recipientBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
return Bytes.concat(recipientBitcoinPKH, hashOfSecretA, lockTimeABytes); return Bytes.concat(recipientBitcoinPKH, hashOfSecretA, lockTimeABytes);
} }
/** Returns trade-info extracted from MESSAGE payload sent by trade partner/recipient, or null if not valid. */ /** 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) { public static OfferMessageData extractOfferMessageData(byte[] messageData) {
if (messageData == null || messageData.length != 20 + 20 + 8) if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
return null; return null;
OfferMessageData offerMessageData = new OfferMessageData(); OfferMessageData offerMessageData = new OfferMessageData();
offerMessageData.recipientBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20); offerMessageData.partnerBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40); offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40); offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
return offerMessageData; return offerMessageData;
} }
/** Returns trade-info MESSAGE payload for AT creator to send to AT. */ /** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
public static byte[] buildTradeMessage(String recipientQortalAddress, byte[] recipientBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int lockTimeB) { public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int lockTimeB) {
byte[] data = new byte[32 + 32 + 32]; byte[] data = new byte[TRADE_MESSAGE_LENGTH];
byte[] recipientQortalAddressBytes = Base58.decode(recipientQortalAddress); byte[] recipientQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
byte[] lockTimeBBytes = BitTwiddling.toBEByteArray((long) lockTimeB); byte[] lockTimeBBytes = BitTwiddling.toBEByteArray((long) lockTimeB);
System.arraycopy(recipientQortalAddressBytes, 0, data, 0, recipientQortalAddressBytes.length); System.arraycopy(recipientQortalAddressBytes, 0, data, 0, recipientQortalAddressBytes.length);
System.arraycopy(recipientBitcoinPKH, 0, data, 32, recipientBitcoinPKH.length); System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
System.arraycopy(lockTimeBBytes, 0, data, 56, lockTimeBBytes.length); System.arraycopy(lockTimeBBytes, 0, data, 56, lockTimeBBytes.length);
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length); System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length); System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
@ -745,9 +781,9 @@ public class BTCACCT {
return data; return data;
} }
/** Returns refund MESSAGE payload for AT creator to cancel trade AT. */ /** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
public static byte[] buildRefundMessage(String creatorQortalAddress) { public static byte[] buildCancelMessage(String creatorQortalAddress) {
byte[] data = new byte[32]; byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress); byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length); System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
@ -755,31 +791,33 @@ public class BTCACCT {
return data; return data;
} }
/** Returns redeem MESSAGE payload for trade partner/recipient to send to AT. */ /** Returns 'redeem' MESSAGE payload for trade partner/ to send to AT. */
public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB) { public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB, String qortalReceiveAddress) {
byte[] data = new byte[32 + 32]; byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
byte[] qortalReceiveAddressBytes = Base58.decode(qortalReceiveAddress);
System.arraycopy(secretA, 0, data, 0, secretA.length); System.arraycopy(secretA, 0, data, 0, secretA.length);
System.arraycopy(secretB, 0, data, 32, secretB.length); System.arraycopy(secretB, 0, data, 32, secretB.length);
System.arraycopy(qortalReceiveAddressBytes, 0, data, 64, qortalReceiveAddressBytes.length);
return data; return data;
} }
/** Returns P2SH-B lockTime (epoch seconds) based on trade partner/recipient's MESSAGE timestamp and P2SH-A locktime. */ /** Returns P2SH-B lockTime (epoch seconds) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
public static int calcLockTimeB(long recipientMessageTimestamp, int lockTimeA) { public static int calcLockTimeB(long offerMessageTimestamp, int lockTimeA) {
// lockTimeB is halfway between recipientMessageTimesamp and lockTimeA // lockTimeB is halfway between offerMessageTimesamp and lockTimeA
return (int) ((lockTimeA + (recipientMessageTimestamp / 1000L)) / 2L); return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
} }
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException { public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
String atAddress = crossChainTradeData.qortalAtAddress; String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalRecipient; String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(atAddress, null, null, null); List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(atAddress, null, null, null);
if (messageTransactionsData == null) if (messageTransactionsData == null)
return null; return null;
// Find redeem message // Find 'redeem' message
for (MessageTransactionData messageTransactionData : messageTransactionsData) { for (MessageTransactionData messageTransactionData : messageTransactionsData) {
// Check message payload type/encryption // Check message payload type/encryption
if (messageTransactionData.isText() || messageTransactionData.isEncrypted()) if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
@ -787,7 +825,7 @@ public class BTCACCT {
// Check message payload size // Check message payload size
byte[] messageData = messageTransactionData.getData(); byte[] messageData = messageTransactionData.getData();
if (messageData.length != 32 + 32) if (messageData.length != REDEEM_MESSAGE_LENGTH)
// Wrong payload length // Wrong payload length
continue; continue;

View File

@ -6,6 +6,7 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -13,8 +14,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeData { public class CrossChainTradeData {
public enum Mode { OFFER, TRADE };
// Properties // Properties
@Schema(description = "AT's Qortal address") @Schema(description = "AT's Qortal address")
@ -29,9 +28,6 @@ public class CrossChainTradeData {
@Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)") @Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)")
public byte[] creatorBitcoinPKH; public byte[] creatorBitcoinPKH;
@Schema(description = "AT creator's Bitcoin receiving public-key-hash (PKH)")
public byte[] creatorReceiveBitcoinPKH;
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)") @Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
public long creationTimestamp; public long creationTimestamp;
@ -53,7 +49,7 @@ public class CrossChainTradeData {
public long qortAmount; public long qortAmount;
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)") @Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
public String qortalRecipient; public String qortalPartnerAddress;
@Schema(description = "Timestamp when AT switched to trade mode") @Schema(description = "Timestamp when AT switched to trade mode")
public Long tradeModeTimestamp; public Long tradeModeTimestamp;
@ -68,7 +64,7 @@ public class CrossChainTradeData {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long expectedBitcoin; public long expectedBitcoin;
public Mode mode; public BTCACCT.Mode mode;
@Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout") @Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
public Integer lockTimeA; public Integer lockTimeA;
@ -77,10 +73,7 @@ public class CrossChainTradeData {
public Integer lockTimeB; public Integer lockTimeB;
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)") @Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
public byte[] recipientBitcoinPKH; public byte[] partnerBitcoinPKH;
@Schema(description = "Whether AT has paid out to trade partner")
public Boolean hasRedeemed;
// Constructors // Constructors
@ -102,20 +95,10 @@ public class CrossChainTradeData {
@XmlElement(name = "recipientBitcoinAddress") @XmlElement(name = "recipientBitcoinAddress")
@Schema(description = "Trade partner's trading Bitcoin PKH in address form") @Schema(description = "Trade partner's trading Bitcoin PKH in address form")
public String getRecipientBitcoinAddress() { public String getRecipientBitcoinAddress() {
if (this.recipientBitcoinPKH == null) if (this.partnerBitcoinPKH == null)
return null; return null;
return BTC.getInstance().pkhToAddress(this.recipientBitcoinPKH); return BTC.getInstance().pkhToAddress(this.partnerBitcoinPKH);
}
// We can represent BitcoinPKH as an address
@XmlElement(name = "creatorBitcoinReceivingAddress")
@Schema(description = "AT creator's Bitcoin receiving address")
public String getCreatorBitcoinReceivingAddress() {
if (this.creatorReceiveBitcoinPKH == null)
return null;
return BTC.getInstance().pkhToAddress(this.creatorReceiveBitcoinPKH);
} }
} }

View File

@ -48,7 +48,7 @@ public class TradeBotData {
private long bitcoinAmount; private long bitcoinAmount;
// Never expose this // Never expose this via API
@XmlTransient @XmlTransient
@Schema(hidden = true) @Schema(hidden = true)
private String xprv58; private String xprv58;
@ -57,6 +57,9 @@ public class TradeBotData {
private Integer lockTimeA; private Integer lockTimeA;
// Could be Bitcoin or Qortal...
private byte[] receivingPublicKeyHash;
protected TradeBotData() { protected TradeBotData() {
/* JAXB */ /* JAXB */
} }
@ -65,7 +68,7 @@ public class TradeBotData {
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress, byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress,
byte[] secret, byte[] hashOfSecret, byte[] secret, byte[] hashOfSecret,
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash,
long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA) { long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingPublicKeyHash) {
this.tradePrivateKey = tradePrivateKey; this.tradePrivateKey = tradePrivateKey;
this.tradeState = tradeState; this.tradeState = tradeState;
this.atAddress = atAddress; this.atAddress = atAddress;
@ -80,6 +83,7 @@ public class TradeBotData {
this.xprv58 = xprv58; this.xprv58 = xprv58;
this.lastTransactionSignature = lastTransactionSignature; this.lastTransactionSignature = lastTransactionSignature;
this.lockTimeA = lockTimeA; this.lockTimeA = lockTimeA;
this.receivingPublicKeyHash = receivingPublicKeyHash;
} }
public byte[] getTradePrivateKey() { public byte[] getTradePrivateKey() {
@ -154,4 +158,8 @@ public class TradeBotData {
this.lockTimeA = lockTimeA; this.lockTimeA = lockTimeA;
} }
public byte[] getReceivingPublicKeyHash() {
return this.receivingPublicKeyHash;
}
} }

View File

@ -23,7 +23,7 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
+ "trade_native_public_key, trade_native_public_key_hash, " + "trade_native_public_key, trade_native_public_key_hash, "
+ "trade_native_address, secret, hash_of_secret, " + "trade_native_address, secret, hash_of_secret, "
+ "trade_foreign_public_key, trade_foreign_public_key_hash, " + "trade_foreign_public_key, trade_foreign_public_key_hash, "
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a " + "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_public_key_hash "
+ "FROM TradeBotStates " + "FROM TradeBotStates "
+ "WHERE trade_private_key = ?"; + "WHERE trade_private_key = ?";
@ -50,13 +50,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
Integer lockTimeA = resultSet.getInt(13); Integer lockTimeA = resultSet.getInt(13);
if (lockTimeA == 0 && resultSet.wasNull()) if (lockTimeA == 0 && resultSet.wasNull())
lockTimeA = null; lockTimeA = null;
byte[] receivingPublicKeyHash = resultSet.getBytes(14);
return new TradeBotData(tradePrivateKey, tradeState, return new TradeBotData(tradePrivateKey, tradeState,
atAddress, atAddress,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secret, hashOfSecret, secret, hashOfSecret,
tradeForeignPublicKey, tradeForeignPublicKeyHash, tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA); bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingPublicKeyHash);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch trade-bot trading state from repository", e); throw new DataException("Unable to fetch trade-bot trading state from repository", e);
} }
@ -68,7 +69,7 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
+ "trade_native_public_key, trade_native_public_key_hash, " + "trade_native_public_key, trade_native_public_key_hash, "
+ "trade_native_address, secret, hash_of_secret, " + "trade_native_address, secret, hash_of_secret, "
+ "trade_foreign_public_key, trade_foreign_public_key_hash, " + "trade_foreign_public_key, trade_foreign_public_key_hash, "
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a " + "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_public_key_hash "
+ "FROM TradeBotStates"; + "FROM TradeBotStates";
List<TradeBotData> allTradeBotData = new ArrayList<>(); List<TradeBotData> allTradeBotData = new ArrayList<>();
@ -98,13 +99,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
Integer lockTimeA = resultSet.getInt(14); Integer lockTimeA = resultSet.getInt(14);
if (lockTimeA == 0 && resultSet.wasNull()) if (lockTimeA == 0 && resultSet.wasNull())
lockTimeA = null; lockTimeA = null;
byte[] receivingPublicKeyHash = resultSet.getBytes(15);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
atAddress, atAddress,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secret, hashOfSecret, secret, hashOfSecret,
tradeForeignPublicKey, tradeForeignPublicKeyHash, tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA); bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingPublicKeyHash);
allTradeBotData.add(tradeBotData); allTradeBotData.add(tradeBotData);
} while (resultSet.next()); } while (resultSet.next());
@ -130,7 +132,8 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount()) .bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
.bind("xprv58", tradeBotData.getXprv58()) .bind("xprv58", tradeBotData.getXprv58())
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature()) .bind("last_transaction_signature", tradeBotData.getLastTransactionSignature())
.bind("locktime_a", tradeBotData.getLockTimeA()); .bind("locktime_a", tradeBotData.getLockTimeA())
.bind("receiving_public_key_hash", tradeBotData.getReceivingPublicKeyHash());
try { try {
saveHelper.execute(this.repository); saveHelper.execute(this.repository);

View File

@ -626,7 +626,7 @@ public class HSQLDBDatabaseUpdates {
+ "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, " + "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, "
+ "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, " + "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
+ "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, " + "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, "
+ "PRIMARY KEY (trade_private_key))"); + "receiving_public_key_hash VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))");
break; break;
default: default:

View File

@ -19,7 +19,6 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.Block; import org.qortal.block.Block;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
@ -65,7 +64,7 @@ public class AtTests extends Common {
public void testCompile() { public void testCompile() {
Account deployer = Common.getTestAccount(null, "chloe"); Account deployer = Common.getTestAccount(null, "chloe");
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout, bitcoinReceivePublicKeyHash); byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
} }
@ -73,10 +72,10 @@ public class AtTests extends Common {
public void testDeploy() throws DataException { public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
@ -90,10 +89,10 @@ public class AtTests extends Common {
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance); assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance; expectedBalance = partnersInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT); actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-deployment balance incorrect", expectedBalance, actualBalance); assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning // Test orphaning
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
@ -108,10 +107,10 @@ public class AtTests extends Common {
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance; expectedBalance = partnersInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT); actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
} }
} }
@ -120,10 +119,10 @@ public class AtTests extends Common {
public void testOfferCancel() throws DataException { public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
@ -132,12 +131,12 @@ public class AtTests extends Common {
long deployAtFee = deployAtTransaction.getTransactionData().getFee(); long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT // Send creator's address to AT, instead of typical partner's address
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0); byte[] partnerAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, partnerAddressBytes, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee(); long messageFee = messageTransaction.getTransactionData().getFee();
// Refund should happen 1st block after receiving recipient address // Refund should happen 1st block after receiving 'cancel' message
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
long expectedMinimumBalance = deployersPostDeploymentBalance; long expectedMinimumBalance = deployersPostDeploymentBalance;
@ -154,6 +153,10 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
// Test orphaning // Test orphaning
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
@ -169,21 +172,21 @@ public class AtTests extends Common {
public void testTradingInfoProcessing() throws DataException { public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository); Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@ -199,16 +202,16 @@ public class AtTests extends Common {
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
// AT should be in TRADE mode // AT should be in TRADE mode
assertEquals(CrossChainTradeData.Mode.TRADE, tradeData.mode); assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly // Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA)); assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner/recipient Qortal address was extracted correctly // Check trade partner Qortal address was extracted correctly
assertEquals(recipient.getAddress(), tradeData.qortalRecipient); assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner/recipient's Bitcoin PKH was extracted correctly // Check trade partner's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.recipientBitcoinPKH)); assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
// Test orphaning // Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@ -226,30 +229,30 @@ public class AtTests extends Common {
public void testIncorrectTradeSender() throws DataException { public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR // Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress); MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance; long expectedBalance = partnersInitialBalance;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT); long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance); assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress); describeAt(repository, atAddress);
@ -257,7 +260,7 @@ public class AtTests extends Common {
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData); CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
// AT should still be in OFFER mode // AT should still be in OFFER mode
assertEquals(CrossChainTradeData.Mode.OFFER, tradeData.mode); assertEquals(BTCACCT.Mode.OFFERING, tradeData.mode);
} }
} }
@ -266,21 +269,21 @@ public class AtTests extends Common {
public void testAutomaticTradeRefund() throws DataException { public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository); Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@ -298,6 +301,10 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode);
// Test orphaning // Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@ -313,29 +320,29 @@ public class AtTests extends Common {
public void testCorrectSecretsCorrectSender() throws DataException { public void testCorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account // Send correct secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB); messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, recipient, messageData, atAddress); messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block // AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -347,18 +354,22 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished()); assertTrue(atData.getIsFinished());
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; // AT should be in REDEEMED mode
long actualBalance = recipient.getConfirmedBalance(Asset.QORT); CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REDEEMED, tradeData.mode);
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance); long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem // Orphan redeem
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee(); expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = recipient.getConfirmedBalance(Asset.QORT); actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance); assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state // Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -372,11 +383,11 @@ public class AtTests extends Common {
public void testCorrectSecretsIncorrectSender() throws DataException { public void testCorrectSecretsIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
long deployAtFee = deployAtTransaction.getTransactionData().getFee(); long deployAtFee = deployAtTransaction.getTransactionData().getFee();
@ -384,19 +395,19 @@ public class AtTests extends Common {
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
// Send correct secrets to AT, but from wrong account // Send correct secrets to AT, but from wrong account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB); messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress); messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
@ -409,10 +420,14 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
long expectedBalance = recipientsInitialBalance; // AT should still be in TRADE mode
long actualBalance = recipient.getConfirmedBalance(Asset.QORT); CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
} }
@ -423,10 +438,10 @@ public class AtTests extends Common {
public void testIncorrectSecretsCorrectSender() throws DataException { public void testIncorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
long deployAtFee = deployAtTransaction.getTransactionData().getFee(); long deployAtFee = deployAtTransaction.getTransactionData().getFee();
@ -434,12 +449,12 @@ public class AtTests extends Common {
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis(); long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT // Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
// Give AT time to process message // Give AT time to process message
@ -449,8 +464,8 @@ public class AtTests extends Common {
byte[] wrongSecret = new byte[32]; byte[] wrongSecret = new byte[32];
Random random = new Random(); Random random = new Random();
random.nextBytes(wrongSecret); random.nextBytes(wrongSecret);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB); messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, recipient, messageData, atAddress); messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -462,14 +477,18 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress); ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee(); // AT should still be in TRADE mode
long actualBalance = recipient.getConfirmedBalance(Asset.QORT); CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Send incorrect secrets to AT, from correct account // Send incorrect secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret); messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, recipient, messageData, atAddress); messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
@ -480,10 +499,14 @@ public class AtTests extends Common {
atData = repository.getATRepository().fromATAddress(atAddress); atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished()); assertFalse(atData.getIsFinished());
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() * 2; // AT should still be in TRADE mode
actualBalance = recipient.getConfirmedBalance(Asset.QORT); tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
} }
@ -494,10 +517,10 @@ public class AtTests extends Common {
public void testDescribeDeployed() throws DataException { public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
@ -528,7 +551,7 @@ public class AtTests extends Common {
} }
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout, bitcoinReceivePublicKeyHash); byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis(); long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference(); byte[] lastReference = deployer.getLastReference();
@ -611,6 +634,7 @@ public class AtTests extends Common {
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n" System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n" + "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n" + "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n" + "\tcurrent balance: %s QORT,\n"
@ -618,9 +642,9 @@ public class AtTests extends Common {
+ "\tHASH160 of secret-B: %s,\n" + "\tHASH160 of secret-B: %s,\n"
+ "\tredeem payout: %s QORT,\n" + "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n" + "\texpected bitcoin: %s BTC,\n"
+ "\treceiving bitcoin address: %s,\n"
+ "\tcurrent block height: %d,\n", + "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress, tradeData.qortalAtAddress,
tradeData.mode.name(),
tradeData.qortalCreator, tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp), epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance), Amounts.prettyAmount(tradeData.qortBalance),
@ -628,26 +652,19 @@ public class AtTests extends Common {
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40), HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.qortAmount), Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedBitcoin), Amounts.prettyAmount(tradeData.expectedBitcoin),
BTC.getInstance().pkhToAddress(tradeData.creatorReceiveBitcoinPKH),
currentBlockHeight)); currentBlockHeight));
// Are we in 'offer' or 'trade' stage? if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
if (tradeData.tradeRefundHeight == null) { System.out.println(String.format("\trefund height: block %d,\n"
// Offer
System.out.println(String.format("\tstatus: 'offer mode'"));
} else {
// Trade
System.out.println(String.format("\tstatus: 'trade mode',\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n" + "\tHASH160 of secret-A: %s,\n"
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n" + "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
+ "\tBitcoin P2SH-B nLockTime: %d (%s),\n" + "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
+ "\ttrade recipient: %s", + "\ttrade partner: %s",
tradeData.tradeRefundHeight, tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40), HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L), tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L), tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L),
tradeData.qortalRecipient)); tradeData.qortalPartnerAddress));
} }
} }

View File

@ -2,13 +2,10 @@ package org.qortal.test.btcacct;
import java.security.Security; import java.security.Security;
import org.bitcoinj.core.Address;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCACCT;
import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.DeployAtTransactionData;
@ -37,7 +34,7 @@ public class DeployAT {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <trade-timeout> <your bitcoin receive address")); System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <trade-timeout>"));
System.err.println(String.format("example: DeployAT " System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n" + "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n" + "\t80.4020 \\\n"
@ -45,13 +42,12 @@ public class DeployAT {
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n" + "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t123.456 \\\n" + "\t123.456 \\\n"
+ "\t10080 \\\n" + "\t10080"));
+ "\tn2iQZCtKZ5SrFDJENGJkd4RpAuQp3SEoix"));
System.exit(1); System.exit(1);
} }
public static void main(String[] args) { public static void main(String[] args) {
if (args.length != 8) if (args.length != 7)
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
@ -64,7 +60,6 @@ public class DeployAT {
byte[] secretHash = null; byte[] secretHash = null;
long fundingAmount = 0; long fundingAmount = 0;
int tradeTimeout = 0; int tradeTimeout = 0;
byte[] bitcoinReceivePublicKeyHash = null;
int argIndex = 0; int argIndex = 0;
try { try {
@ -95,12 +90,6 @@ public class DeployAT {
tradeTimeout = Integer.parseInt(args[argIndex++]); tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 60 || tradeTimeout > 50000) if (tradeTimeout < 60 || tradeTimeout > 50000)
usage("Trade timeout (minutes) must be between 60 and 50000"); usage("Trade timeout (minutes) must be between 60 and 50000");
Address receiveAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), args[argIndex++]);
if (receiveAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Bitcoin receive address must be P2PKH form");
bitcoinReceivePublicKeyHash = receiveAddress.getHash();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
} }
@ -125,7 +114,7 @@ public class DeployAT {
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT // Deploy AT
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout, bitcoinReceivePublicKeyHash); byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis(); long txTimestamp = System.currentTimeMillis();