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")
public Integer tradeTimeout;
@Schema(description = "Bitcoin address for receiving", example = "1NCTG9oLk41bU6pcehLNo9DVJup77EHAVx")
public String receiveAddress;
public CrossChainBuildRequest() {
}

View File

@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
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;
@Schema(description = "Qortal trade AT address")

View File

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

View File

@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
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;
@Schema(description = "Qortal AT address")
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 CrossChainTradeRequest() {

View File

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

View File

@ -59,7 +59,6 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.crosschain.CrossChainTradeData.Mode;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
@ -182,23 +181,11 @@ public class CrossChainResource {
if (tradeRequest.bitcoinAmount <= 0)
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()) {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
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();
byte[] lastReference = creatorAccount.getLastReference();
@ -233,11 +220,11 @@ public class CrossChainResource {
}
@POST
@Path("/tradeoffer/recipient")
@Path("/tradeoffer/trademessage")
@Operation(
summary = "Builds raw, unsigned 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>"
+ "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>"
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 signature of 'offer' MESSAGE from trade partner.<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.",
requestBody = @RequestBody(
required = true,
@ -259,7 +246,7 @@ public class CrossChainResource {
}
)
@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);
byte[] tradePublicKey = tradeRequest.tradePublicKey;
@ -277,11 +264,11 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress);
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);
// 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);
TransactionData transactionData = repository.getTransactionRepository().fromSignature(tradeRequest.messageTransactionSignature);
@ -299,7 +286,7 @@ public class CrossChainResource {
// Good to make MESSAGE
byte[] aliceForeignPublicKeyHash = offerMessageData.recipientBitcoinPKH;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
@ -316,12 +303,12 @@ public class CrossChainResource {
}
@POST
@Path("/tradeoffer/secret")
@Path("/tradeoffer/redeemmessage")
@Operation(
summary = "Builds raw, unsigned MESSAGE transaction that sends secrets to AT, releasing funds to recipient",
description = "Specify address of cross-chain AT that needs to be messaged, and both 32-byte secrets.<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>"
+ "You need to sign output with account the AT considers the 'recipient' otherwise the MESSAGE transaction will be invalid.",
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, 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 any other mode will be ignored, but still cost fees to send!<br>"
+ "You need to sign output with account the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
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})
public String sendSecret(CrossChainSecretRequest secretRequest) {
public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
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);
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)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER)
if (crossChainTradeData.mode != BTCACCT.Mode.TRADING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, recipientPublicKey);
String recipientAddress = recipientAccount.getAddress();
String partnerAddress = Crypto.toAddress(partnerPublicKey);
// MESSAGE must come from address that AT considers trade partner / 'recipient'
if (!crossChainTradeData.qortalRecipient.equals(recipientAddress))
// MESSAGE must come from address that AT considers trade partner
if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// Good to make MESSAGE
byte[] messageData = BTCACCT.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB);
byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, messageData);
byte[] messageData = BTCACCT.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB, secretRequest.receivingAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, partnerPublicKey, secretRequest.atAddress, messageData);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
@ -387,7 +376,7 @@ public class CrossChainResource {
@DELETE
@Path("/tradeoffer")
@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>"
+ "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.",
@ -411,7 +400,7 @@ public class CrossChainResource {
}
)
@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);
byte[] tradePublicKey = cancelRequest.tradePublicKey;
@ -426,17 +415,17 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
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);
// 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);
// Good to make MESSAGE
String atCreatorAddress = crossChainTradeData.qortalCreator;
byte[] messageData = BTCACCT.buildRefundMessage(atCreatorAddress);
byte[] messageData = BTCACCT.buildCancelMessage(atCreatorAddress);
byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, cancelRequest.atAddress, messageData);
@ -516,7 +505,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, templateRequest.atAddress);
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);
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);
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);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
@ -732,7 +721,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
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);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
@ -874,7 +863,7 @@ public class CrossChainResource {
ATData atData = fetchAtDataWithChecking(repository, redeemRequest.atAddress);
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);
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
@ -1031,15 +1020,18 @@ public class CrossChainResource {
if (!BTC.getInstance().isValidXprv(tradeBotRespondRequest.xprv58))
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
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, atAddress);
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);
boolean result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58);
boolean result = TradeBot.startResponse(repository, crossChainTradeData, tradeBotRespondRequest.xprv58, tradeBotRespondRequest.receivingAddress);
return result ? "true" : "false";
} catch (DataException e) {

View File

@ -40,6 +40,7 @@ import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
public class TradeBot {
@ -133,7 +134,7 @@ public class TradeBot {
String aTType = "ACCT";
String tags = "ACCT QORT BTC";
byte[] creationBytes = BTCACCT.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount,
tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout, bitcoinReceivePublicKeyHash);
tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount;
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
@ -150,7 +151,7 @@ public class TradeBot {
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretB, hashOfSecretB,
tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.bitcoinAmount, null, null, null);
tradeBotCreateRequest.bitcoinAmount, null, null, null, bitcoinReceivePublicKeyHash);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
@ -188,7 +189,7 @@ public class TradeBot {
* <p>
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
* <p>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A<b>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
* with the Bitcoin amount expected by 'Bob'.
* <p>
* 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
* @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[] secretA = generateSecret();
byte[] hashOfSecretA = Crypto.hash160(secretA);
@ -213,6 +214,7 @@ public class TradeBot {
byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
// We need to generate lockTime-A: halfway of refundTimeout from now
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L);
@ -222,7 +224,7 @@ public class TradeBot {
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretA, hashOfSecretA,
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
String tradeForeignAddress = BTC.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
@ -499,7 +501,7 @@ public class TradeBot {
if (offerMessageData == null)
continue;
byte[] aliceForeignPublicKeyHash = offerMessageData.recipientBitcoinPKH;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
// Determine P2SH-A address and confirm funded
@ -592,11 +594,11 @@ public class TradeBot {
}
// We're waiting for AT to be in TRADE mode
if (crossChainTradeData.mode != CrossChainTradeData.Mode.TRADE)
if (crossChainTradeData.mode != BTCACCT.Mode.TRADING)
return;
// 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.
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",
tradeBotData.getAtAddress(),
crossChainTradeData.qortalRecipient,
crossChainTradeData.qortalPartnerAddress,
tradeBotData.getTradeNativeAddress(),
p2shAddress));
@ -701,7 +703,7 @@ public class TradeBot {
// AT yet to process MESSAGE
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);
Long balance = BTC.getInstance().getBalance(p2shAddress);
@ -716,7 +718,7 @@ public class TradeBot {
Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
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);
@ -783,9 +785,10 @@ public class TradeBot {
// Secret not revealed at this time
return;
// Send MESSAGE to AT using both secrets
// Send 'redeem' MESSAGE to AT using both secrets
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());
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
if (!crossChainTradeData.hasRedeemed) {
if (crossChainTradeData.mode != BTCACCT.Mode.REDEEMED) {
tradeBotData.setState(TradeBotData.State.BOB_REFUNDED);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
@ -864,13 +867,13 @@ public class TradeBot {
// 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);
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
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);

View File

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

View File

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

View File

@ -48,7 +48,7 @@ public class TradeBotData {
private long bitcoinAmount;
// Never expose this
// Never expose this via API
@XmlTransient
@Schema(hidden = true)
private String xprv58;
@ -57,6 +57,9 @@ public class TradeBotData {
private Integer lockTimeA;
// Could be Bitcoin or Qortal...
private byte[] receivingPublicKeyHash;
protected TradeBotData() {
/* JAXB */
}
@ -65,7 +68,7 @@ public class TradeBotData {
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress,
byte[] secret, byte[] hashOfSecret,
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.tradeState = tradeState;
this.atAddress = atAddress;
@ -80,6 +83,7 @@ public class TradeBotData {
this.xprv58 = xprv58;
this.lastTransactionSignature = lastTransactionSignature;
this.lockTimeA = lockTimeA;
this.receivingPublicKeyHash = receivingPublicKeyHash;
}
public byte[] getTradePrivateKey() {
@ -154,4 +158,8 @@ public class TradeBotData {
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_address, secret, hash_of_secret, "
+ "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 "
+ "WHERE trade_private_key = ?";
@ -50,13 +50,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
Integer lockTimeA = resultSet.getInt(13);
if (lockTimeA == 0 && resultSet.wasNull())
lockTimeA = null;
byte[] receivingPublicKeyHash = resultSet.getBytes(14);
return new TradeBotData(tradePrivateKey, tradeState,
atAddress,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secret, hashOfSecret,
tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA);
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingPublicKeyHash);
} catch (SQLException 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_address, secret, hash_of_secret, "
+ "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";
List<TradeBotData> allTradeBotData = new ArrayList<>();
@ -98,13 +99,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
Integer lockTimeA = resultSet.getInt(14);
if (lockTimeA == 0 && resultSet.wasNull())
lockTimeA = null;
byte[] receivingPublicKeyHash = resultSet.getBytes(15);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
atAddress,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secret, hashOfSecret,
tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA);
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingPublicKeyHash);
allTradeBotData.add(tradeBotData);
} while (resultSet.next());
@ -130,7 +132,8 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
.bind("xprv58", tradeBotData.getXprv58())
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature())
.bind("locktime_a", tradeBotData.getLockTimeA());
.bind("locktime_a", tradeBotData.getLockTimeA())
.bind("receiving_public_key_hash", tradeBotData.getReceivingPublicKeyHash());
try {
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_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, "
+ "PRIMARY KEY (trade_private_key))");
+ "receiving_public_key_hash VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))");
break;
default:

View File

@ -19,7 +19,6 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
@ -65,7 +64,7 @@ public class AtTests extends Common {
public void testCompile() {
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());
}
@ -73,10 +72,10 @@ public class AtTests extends Common {
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
@ -90,10 +89,10 @@ public class AtTests extends Common {
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
expectedBalance = partnersInitialBalance;
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
BlockUtils.orphanLastBlock(repository);
@ -108,10 +107,10 @@ public class AtTests extends Common {
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
expectedBalance = partnersInitialBalance;
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 {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount();
@ -132,12 +131,12 @@ public class AtTests extends Common {
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
// Send creator's address to AT, instead of typical partner's address
byte[] partnerAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, partnerAddressBytes, atAddress);
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);
long expectedMinimumBalance = deployersPostDeploymentBalance;
@ -154,6 +153,10 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
@ -169,21 +172,21 @@ public class AtTests extends Common {
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// 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);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@ -199,16 +202,16 @@ public class AtTests extends Common {
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(CrossChainTradeData.Mode.TRADE, tradeData.mode);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner/recipient Qortal address was extracted correctly
assertEquals(recipient.getAddress(), tradeData.qortalRecipient);
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner/recipient's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.recipientBitcoinPKH));
// Check trade partner's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@ -226,30 +229,30 @@ public class AtTests extends Common {
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// 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);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
long expectedBalance = partnersInitialBalance;
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);
@ -257,7 +260,7 @@ public class AtTests extends Common {
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
// 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 {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// 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);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@ -298,6 +301,10 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@ -313,29 +320,29 @@ public class AtTests extends Common {
public void testCorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// 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);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB);
messageTransaction = sendMessage(repository, recipient, messageData, atAddress);
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -347,18 +354,22 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
// AT should be in REDEEMED mode
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
BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
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
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -372,11 +383,11 @@ public class AtTests extends Common {
public void testCorrectSecretsIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
@ -384,19 +395,19 @@ public class AtTests extends Common {
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// 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);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// 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);
// AT should NOT send funds in the next block
@ -409,10 +420,14 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
long expectedBalance = recipientsInitialBalance;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
// AT should still be in TRADE mode
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);
}
@ -423,10 +438,10 @@ public class AtTests extends Common {
public void testIncorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
@ -434,12 +449,12 @@ public class AtTests extends Common {
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long recipientMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// 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);
// Give AT time to process message
@ -449,8 +464,8 @@ public class AtTests extends Common {
byte[] wrongSecret = new byte[32];
Random random = new Random();
random.nextBytes(wrongSecret);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB);
messageTransaction = sendMessage(repository, recipient, messageData, atAddress);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -462,14 +477,18 @@ public class AtTests extends Common {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
// AT should still be in TRADE mode
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
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret);
messageTransaction = sendMessage(repository, recipient, messageData, atAddress);
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
BlockUtils.mintBlock(repository);
@ -480,10 +499,14 @@ public class AtTests extends Common {
atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
// AT should still be in TRADE mode
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);
}
@ -494,10 +517,10 @@ public class AtTests extends Common {
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
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 recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
@ -528,7 +551,7 @@ public class AtTests extends Common {
}
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();
byte[] lastReference = deployer.getLastReference();
@ -611,6 +634,7 @@ public class AtTests extends Common {
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
@ -618,9 +642,9 @@ public class AtTests extends Common {
+ "\tHASH160 of secret-B: %s,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n"
+ "\treceiving bitcoin address: %s,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode.name(),
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
@ -628,26 +652,19 @@ public class AtTests extends Common {
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedBitcoin),
BTC.getInstance().pkhToAddress(tradeData.creatorReceiveBitcoinPKH),
currentBlockHeight));
// Are we in 'offer' or 'trade' stage?
if (tradeData.tradeRefundHeight == null) {
// Offer
System.out.println(String.format("\tstatus: 'offer mode'"));
} else {
// Trade
System.out.println(String.format("\tstatus: 'trade mode',\n"
+ "\trefund height: block %d,\n"
if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
System.out.println(String.format("\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
+ "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
+ "\ttrade recipient: %s",
+ "\ttrade partner: %s",
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 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 org.bitcoinj.core.Address;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
@ -37,7 +34,7 @@ public class DeployAT {
if (error != null)
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 "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n"
@ -45,13 +42,12 @@ public class DeployAT {
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t123.456 \\\n"
+ "\t10080 \\\n"
+ "\tn2iQZCtKZ5SrFDJENGJkd4RpAuQp3SEoix"));
+ "\t10080"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 8)
if (args.length != 7)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
@ -64,7 +60,6 @@ public class DeployAT {
byte[] secretHash = null;
long fundingAmount = 0;
int tradeTimeout = 0;
byte[] bitcoinReceivePublicKeyHash = null;
int argIndex = 0;
try {
@ -95,12 +90,6 @@ public class DeployAT {
tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 60 || tradeTimeout > 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) {
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)));
// 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());
long txTimestamp = System.currentTimeMillis();