More work on cross-chain trading, including API calls.

Added API calls to aid Qortal-side of cross-chain trading.
POST /crosschain/build - for building Qortal AT
POST /crosschain/tradeoffer/recipient - for sending trade partner/recipient to AT
POST /crosschain/tradeoffer/secret - for sending secret to AT
DELETE /crosschain/tradeoffer - for cancelling AT

More fixes regarding Blocks processing/orphaning ATs.
More fixes regarding sending/receiving blocks containing AT data.
AT-related fix to genesis block.

Improved cross-chain trading AT code, removing offer-mode timeout
and replacing that with allowing AT creator to cancel offer/end AT
by sending AT the creator's own address as trade partner/recipient.
After all, they're not going to trade with themselves.

Added assertion to check BTCACCT.CODE_BYTES_HASH matches compiled code hash.

Added cross-chain AT's 'mode' for easier diagnosis, either OFFER or TRADE.

We can't use AT's signature to generate AT address because address is needed
before DEPLOY_AT transaction is signed. So we use a hash of signature-less
transaction bytes.

Corresponding changes to tests.
This commit is contained in:
catbref 2020-04-21 09:31:09 +01:00
parent 8baf42765e
commit 94d18538d8
19 changed files with 663 additions and 127 deletions

View File

@ -0,0 +1,37 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBuildRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000")
public BigDecimal initialQortAmount;
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
public BigDecimal finalQortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
public BigDecimal fundingQortAmount;
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
public byte[] secretHash;
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
public BigDecimal bitcoinAmount;
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
public Integer tradeTimeout;
public CrossChainBuildRequest() {
}
}

View File

@ -0,0 +1,19 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
public String atAddress;
public CrossChainCancelRequest() {
}
}

View File

@ -0,0 +1,22 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest {
@Schema(description = "AT's 'recipient' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] recipientPublicKey;
public String atAddress;
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
public byte[] secret;
public CrossChainSecretRequest() {
}
}

View File

@ -0,0 +1,21 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
public String atAddress;
public String recipient;
public CrossChainTradeRequest() {
}
}

View File

@ -5,23 +5,34 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.ciyam.at.MachineState;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.CrossChainCancelRequest;
import org.qortal.api.model.CrossChainSecretRequest;
import org.qortal.api.model.CrossChainTradeRequest;
import org.qortal.api.model.CrossChainBuildRequest;
import org.qortal.asset.Asset;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.crosschain.BTCACCT;
@ -29,9 +40,26 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import com.google.common.primitives.Bytes;
@Path("/crosschain")
@Tag(name = "Cross-Chain")
@ -102,4 +130,351 @@ public class CrossChainResource {
}
}
@POST
@Path("/build")
@Operation(
summary = "Build cross-chain trading AT",
description = "Returns raw, unsigned DEPLOY_AT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainBuildRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE})
public String buildTrade(CrossChainBuildRequest tradeRequest) {
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (tradeRequest.secretHash == null || tradeRequest.secretHash.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.tradeTimeout == null)
tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days
else
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.initialQortAmount == null || tradeRequest.initialQortAmount.signum() < 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.finalQortAmount == null || tradeRequest.finalQortAmount.signum() <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.fundingQortAmount == null || tradeRequest.fundingQortAmount.signum() <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
// funding amount must exceed initial + final
if (tradeRequest.fundingQortAmount.compareTo(tradeRequest.initialQortAmount.add(tradeRequest.finalQortAmount)) <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.bitcoinAmount == null || tradeRequest.bitcoinAmount.signum() <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.secretHash, tradeRequest.tradeTimeout, tradeRequest.initialQortAmount, tradeRequest.finalQortAmount, tradeRequest.bitcoinAmount);
long txTimestamp = NTP.getTime();
byte[] lastReference = creatorAccount.getLastReference();
if (lastReference == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
BigDecimal fee = BigDecimal.ZERO;
String name = "QORT-BTC cross-chain trade";
String description = String.format("Qortal-Bitcoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
ValidationResult result = deployAtTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/tradeoffer/recipient")
@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>"
+ "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainTradeRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public String sendTradeRecipient(CrossChainTradeRequest tradeRequest) {
byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (tradeRequest.atAddress == null || !Crypto.isValidAtAddress(tradeRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (tradeRequest.recipient == null || !Crypto.isValidAddress(tradeRequest.recipient))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, tradeRequest.atAddress);
// Determine state of AT
ATStateData atStateData = repository.getATRepository().getLatestATState(tradeRequest.atAddress);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Good to make MESSAGE
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(tradeRequest.recipient), 32, 0);
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, tradeRequest.atAddress, recipientAddressBytes);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/tradeoffer/secret")
@Operation(
summary = "Builds raw, unsigned MESSAGE transaction that sends secret to AT, releasing funds to recipient",
description = "Specify address of cross-chain AT that needs to be messaged, and 32-byte secret.<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.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainSecretRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public String sendSecret(CrossChainSecretRequest secretRequest) {
byte[] recipientPublicKey = secretRequest.recipientPublicKey;
if (recipientPublicKey == null || recipientPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (secretRequest.secret == null || secretRequest.secret.length != 32)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, null, secretRequest.atAddress); // null to skip creator check
// Determine state of AT
ATStateData atStateData = repository.getATRepository().getLatestATState(secretRequest.atAddress);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, recipientPublicKey);
String recipientAddress = recipientAccount.getAddress();
// MESSAGE must come from address that AT considers trade partner / 'recipient'
if (!crossChainTradeData.qortalRecipient.equals(recipientAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// Good to make MESSAGE
byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, secretRequest.secret);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Path("/tradeoffer")
@Operation(
summary = "Builds raw, unsigned 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 same account as the AT creator otherwise the MESSAGE transaction will be invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = CrossChainCancelRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public String cancelTradeOffer(CrossChainCancelRequest cancelRequest) {
byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, cancelRequest.atAddress);
// Determine state of AT
ATStateData atStateData = repository.getATRepository().getLatestATState(cancelRequest.atAddress);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData());
CrossChainTradeData crossChainTradeData = new CrossChainTradeData();
BTCACCT.populateTradeData(crossChainTradeData, dataBytes);
if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Good to make MESSAGE
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
String creatorAddress = creatorAccount.getAddress();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(creatorAddress), 32, 0);
byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, recipientAddressBytes);
return Base58.encode(messageTransactionBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
private ATData fetchAtDataWithChecking(Repository repository, byte[] creatorPublicKey, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Does supplied public key match that of AT?
if (creatorPublicKey != null && !Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
// Must be correct AT - check functionality using code hash
if (!Arrays.equals(atData.getCodeHash(), BTCACCT.CODE_BYTES_HASH))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// No point sending message to AT that's finished
if (atData.getIsFinished())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
return atData;
}
private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, senderPublicKey);
long txTimestamp = NTP.getTime();
byte[] lastReference = creatorAccount.getLastReference();
if (lastReference == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
BigDecimal fee = BigDecimal.ZERO;
BigDecimal amount = BigDecimal.ZERO;
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, atAddress, Asset.QORT, amount, messageData, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
ValidationResult result = messageTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
try {
return MessageTransactionTransformer.toBytes(messageTransactionData);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
}
}

View File

@ -1108,9 +1108,6 @@ public class Block {
* @throws DataException
*/
private ValidationResult areAtsValid() throws DataException {
if (this.blockData.getATCount() == 0)
return ValidationResult.OK;
// Locally generated AT states should be valid so no need to re-execute them
if (this.ourAtStates == this.getATStates()) // Note object reference compare
return ValidationResult.OK;
@ -1207,8 +1204,7 @@ public class Block {
// AT Transactions do not affect block's transaction count
// We've added transactions, so recalculate transactions signature
calcTransactionsSignature();
// AT Transactions do not affect block's transaction signature
}
/** Returns whether block's minter is actually allowed to mint this block. */
@ -1414,7 +1410,7 @@ public class Block {
protected void processAtFeesAndStates() throws DataException {
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atStateData : this.getATStates()) {
for (ATStateData atStateData : this.ourAtStates) {
Account atAccount = new Account(this.repository, atStateData.getATAddress());
// Subtract AT-generated fees from AT accounts

View File

@ -342,6 +342,10 @@ public class GenesisBlock extends Block {
for (Transaction transaction : this.getTransactions())
this.repository.getTransactionRepository().save(transaction.getTransactionData());
// No ATs in genesis block
this.ourAtStates = Collections.emptyList();
this.ourAtFees = BigDecimal.ZERO.setScale(8);
super.process();
}

View File

@ -26,6 +26,7 @@ import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.crypto.Crypto;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
@ -33,10 +34,30 @@ import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/*
* Bob generates Bitcoin private key
* private key required to sign P2SH redeem tx
* private key can be used to create 'secret' (e.g. double-SHA256)
* encrypted private key could be stored in Qortal AT for access by Bob from any node
* Bob creates Qortal AT
* Alice finds Qortal AT and wants to trade
* Alice generates Bitcoin private key
* Alice will need to send Bob her Qortal address and Bitcoin refund address
* Bob sends Alice's Qortal address to Qortal AT
* Qortal AT sends initial QORT payment to Alice (so she has QORT to send message to AT and claim funds)
* Alice receives funds and checks Qortal AT to confirm it's locked to her
* Alice creates/funds Bitcoin P2SH
* Alice requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime
* Bob checks P2SH is funded
* Bob requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime
* Bob uses secret to redeem P2SH
* Qortal core/UI will need to create, and sign, this transaction
* Alice scans P2SH redeem tx and uses secret to redeem Qortal AT
*/
public class BTCACCT {
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("da7271e9aa697112ece632cf2b462fded74843944a704b9d5fd4ae5971f6686f").asBytes(); // SHA256 of AT code bytes
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes
/*
* OP_TUCK (to copy public key to before signature)
@ -62,23 +83,14 @@ public class BTCACCT {
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
/**
* Returns Bitcoin redeem script.
* Returns Bitcoin redeemScript used for cross-chain trading.
* <p>
* <pre>
* OP_TUCK OP_CHECKSIGVERIFY
* OP_HASH160 OP_DUP push(0x14) &lt;refunder pubkeyhash&gt; OP_EQUAL
* OP_IF
* OP_DROP push(0x04 bytes) &lt;refund locktime&gt; OP_CHECKLOCKTIMEVERIFY
* OP_ELSE
* push(0x14) &lt;redeemer pubkeyhash&gt; OP_EQUALVERIFY
* OP_HASH160 push(0x14 bytes) &lt;hash of secret&gt; OP_EQUAL
* OP_ENDIF
* </pre>
* See comments in {@link BTCACCT} for more details.
*
* @param refunderPubKeyHash
* @param senderPubKey
* @param recipientPubKey
* @param lockTime
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
* @return
*/
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
@ -89,14 +101,13 @@ public class BTCACCT {
/**
* Builds a custom transaction to spend P2SH.
*
* @param amount
* @param spendKey
* @param recipientPubKeyHash
* @param fundingOutput
* @param redeemScriptBytes
* @param lockTime
* @param scriptSigBuilder
* @return
* @param amount output amount, should be total of input amounts, less miner fees
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime (optional) transaction nLockTime, used in refund scenario
* @param scriptSigBuilder function for building scriptSig using transaction input signature
* @return Signed Bitcoin transaction for spending P2SH
*/
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
@ -135,6 +146,16 @@ public class BTCACCT {
return transaction;
}
/**
* Returns signed Bitcoin transaction claiming refund from P2SH address.
*
* @param refundAmount refund amount, should be total of input amounts, less miner fees
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @return Signed Bitcoin transaction for refunding P2SH
*/
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
@ -156,6 +177,16 @@ public class BTCACCT {
return buildP2shTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime, refundSigScriptBuilder);
}
/**
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
*
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param secret actual 32-byte secret used when building redeemScript
* @return Signed Bitcoin transaction for redeeming P2SH
*/
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, byte[] secret) {
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
@ -180,27 +211,21 @@ public class BTCACCT {
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
}
/*
* Bob generates Bitcoin private key
* private key required to sign P2SH redeem tx
* private key can be used to create 'secret' (e.g. double-SHA256)
* encrypted private key could be stored in Qortal AT for access by Bob from any node
* Bob creates Qortal AT
* Alice finds Qortal AT and wants to trade
* Alice generates Bitcoin private key
* Alice will need to send Bob her Qortal address and Bitcoin refund address
* Bob sends Alice's Qortal address to Qortal AT
* Qortal AT sends initial QORT payment to Alice (so she has QORT to send message to AT and claim funds)
* Alice receives funds and checks Qortal AT to confirm it's locked to her
* Alice creates/funds Bitcoin P2SH
* Alice requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime
* Bob checks P2SH is funded
* Bob requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime
* Bob uses secret to redeem P2SH
* Qortal core/UI will need to create, and sign, this transaction
* Alice scans P2SH redeem tx and uses secret to redeem Qortal AT
/**
* 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
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
*
* @param qortalCreator Qortal address for AT creator, also used for refunds
* @param secretHash 20-byte HASH160 of 32-byte secret
* @param tradeTimeout how many minutes, from start of 'trade mode' until AT auto-refunds AT creator
* @param initialPayout how much QORT to pay trade partner upon switch to 'trade mode'
* @param redeemPayout how much QORT to pay trade partner if they send correct 32-byte secret to AT
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
* @return
*/
public static byte[] buildQortalAT(String qortalCreator, byte[] secretHash, int offerTimeout, int tradeTimeout, BigDecimal initialPayout, BigDecimal redeemPayout, BigDecimal bitcoinAmount) {
public static byte[] buildQortalAT(String qortalCreator, byte[] secretHash, int tradeTimeout, BigDecimal initialPayout, BigDecimal redeemPayout, BigDecimal bitcoinAmount) {
// Labels for data segment addresses
int addrCounter = 0;
@ -214,7 +239,6 @@ public class BTCACCT {
final int addrSecretHash = addrCounter;
addrCounter += 4;
final int addrOfferTimeout = addrCounter++;
final int addrTradeTimeout = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++;
final int addrRedeemPayoutAmount = addrCounter++;
@ -238,7 +262,6 @@ public class BTCACCT {
final int addrQortalRecipient3 = addrCounter++;
final int addrQortalRecipient4 = addrCounter++;
final int addrOfferRefundTimestamp = addrCounter++;
final int addrTradeRefundTimestamp = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++;
@ -265,10 +288,6 @@ public class BTCACCT {
assert dataByteBuffer.position() == addrSecretHash * MachineState.VALUE_SIZE : "addrSecretHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0));
// Open offer timeout in minutes
assert dataByteBuffer.position() == addrOfferTimeout * MachineState.VALUE_SIZE : "addrOfferTimeout incorrect";
dataByteBuffer.putLong(offerTimeout);
// Trade timeout in minutes
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
dataByteBuffer.putLong(tradeTimeout);
@ -315,6 +334,7 @@ public class BTCACCT {
Integer labelOfferTxLoop = null;
Integer labelCheckOfferTx = null;
Integer labelTradeMode = null;
Integer labelTradeTxLoop = null;
Integer labelCheckTradeTx = null;
@ -330,20 +350,10 @@ public class BTCACCT {
// 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));
// Calculate offer timeout refund 'timestamp' by adding addrOfferTimeout minutes to above 'timestamp', then save into addrOfferRefundTimestamp
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrOfferRefundTimestamp, addrLastTxTimestamp, addrOfferTimeout));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for offer timeout or message from AT owner containing trade partner details */
// Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
// If we're not past offer timeout refund 'timestamp' then look for next transaction
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrOfferRefundTimestamp, calcOffset(codeByteBuffer, labelOfferTxLoop)));
// We've past offer timeout refund 'timestamp' so go refund everything back to AT creator
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
/* Loop, waiting for message from AT owner containing trade partner details, or AT owner's address to cancel offer */
/* Transaction processing loop */
labelOfferTxLoop = codeByteBuffer.position();
@ -385,6 +395,17 @@ public class BTCACCT {
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, addrQortalCreator1, calcOffset(codeByteBuffer, labelTradeMode)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient2, addrQortalCreator2, calcOffset(codeByteBuffer, labelTradeMode)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient3, addrQortalCreator3, calcOffset(codeByteBuffer, labelTradeMode)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient4, addrQortalCreator4, calcOffset(codeByteBuffer, labelTradeMode)));
// Recipient address is AT creator's address, so cancel offer and finish.
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
/* Switch to 'trade mode' */
labelTradeMode = codeByteBuffer.position();
// Send initial payment to recipient so they have enough funds to message AT if all goes well
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount));
@ -478,6 +499,9 @@ public class BTCACCT {
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), BTCACCT.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
@ -486,6 +510,12 @@ public class BTCACCT {
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
/**
* Populates passed CrossChainTradeData with useful info extracted from AT data segment.
*
* @param tradeData
* @param dataBytes
*/
public static void populateTradeData(CrossChainTradeData tradeData, byte[] dataBytes) {
ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes);
byte[] addressBytes = new byte[32];
@ -497,9 +527,6 @@ public class BTCACCT {
tradeData.secretHash = new byte[32];
dataByteBuffer.get(tradeData.secretHash);
// Offer timeout
tradeData.offerRefundTimeout = dataByteBuffer.getLong();
// Trade timeout
tradeData.tradeRefundTimeout = dataByteBuffer.getLong();
@ -532,17 +559,19 @@ public class BTCACCT {
// Qortal recipient (if any)
dataByteBuffer.get(addressBytes);
if (addressBytes[0] != 0)
tradeData.qortalRecipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH));
// Open offer timeout (AT 'timestamp' converted to Qortal block height)
long offerRefundTimestamp = dataByteBuffer.getLong();
tradeData.offerRefundHeight = new Timestamp(offerRefundTimestamp).blockHeight;
// Trade offer timeout (AT 'timestamp' converted to Qortal block height)
long tradeRefundTimestamp = dataByteBuffer.getLong();
if (tradeRefundTimestamp != 0)
if (tradeRefundTimestamp != 0) {
tradeData.mode = CrossChainTradeData.Mode.TRADE;
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
if (addressBytes[0] != 0)
tradeData.qortalRecipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH));
} else {
tradeData.mode = CrossChainTradeData.Mode.OFFER;
}
}
}

View File

@ -11,6 +11,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeData {
public static enum Mode { OFFER, TRADE };
// Properties
@Schema(description = "AT's Qortal address")
@ -37,12 +39,6 @@ public class CrossChainTradeData {
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
public String qortalRecipient;
@Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
public long offerRefundTimeout;
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (before trade begins)")
public int offerRefundHeight;
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
public long tradeRefundTimeout;
@ -52,6 +48,8 @@ public class CrossChainTradeData {
@Schema(description = "Amount, in BTC, that AT creator expects Bitcoin P2SH to pay out (excluding miner fees)")
public BigDecimal expectedBitcoin;
public Mode mode;
// Constructors
// Necessary for JAXB

View File

@ -187,7 +187,7 @@ public class AtTransaction extends Transaction {
// For QORT amounts only: if recipient has no reference yet, then this is their starting reference
if (assetId == Asset.QORT && recipient.getLastReference() == null)
// In Qora1 last reference was set to 64-bytes of zero
// In Qortal we use AT-Transction's signature, which makes more sense
// In Qortal we use AT-Transaction's signature, which makes more sense
recipient.setLastReference(this.atTransactionData.getSignature());
}
}

View File

@ -22,7 +22,9 @@ import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import com.google.common.base.Utf8;
@ -92,11 +94,15 @@ public class DeployAtTransaction extends Transaction {
if (this.deployATTransactionData.getAtAddress() != null)
return;
// For new version, simply use transaction signature
// For new version, simply use transaction transformer
if (this.getVersion() > 1) {
String atAddress = Crypto.toATAddress(this.deployATTransactionData.getSignature());
this.deployATTransactionData.setAtAddress(atAddress);
return;
try {
String atAddress = Crypto.toATAddress(DeployAtTransactionTransformer.toBytesForSigningImpl(this.deployATTransactionData));
this.deployATTransactionData.setAtAddress(atAddress);
return;
} catch (TransformationException e) {
throw new DataException("Unable to generate AT address");
}
}
int blockHeight = this.getHeight();

View File

@ -162,6 +162,9 @@ public class BlockTransformer extends Transformer {
}
}
// Bump byteBuffer over AT states just read in slice
byteBuffer.position(byteBuffer.position() + atBytesLength);
// AT count to reflect the number of states we have
atCount = atStates.size();
@ -295,6 +298,10 @@ public class BlockTransformer extends Transformer {
bytes.write(Ints.toByteArray(atBytesLength));
for (ATStateData atStateData : block.getATStates()) {
// Skip initial states generated by DEPLOY_AT transactions in the same block
if (atStateData.isInitial())
continue;
bytes.write(Base58.decode(atStateData.getATAddress()));
bytes.write(atStateData.getStateHash());
Serialization.serializeBigDecimal(bytes, atStateData.getFees());
@ -319,6 +326,10 @@ public class BlockTransformer extends Transformer {
bytes.write(Ints.toByteArray(blockData.getTransactionCount()));
for (Transaction transaction : block.getTransactions()) {
// Don't serialize AT transactions!
if (transaction.getTransactionData().getType() == TransactionType.AT)
continue;
TransactionData transactionData = transaction.getTransactionData();
bytes.write(Ints.toByteArray(TransactionTransformer.getDataLength(transactionData)));
bytes.write(TransactionTransformer.toBytes(transactionData));

View File

@ -61,7 +61,7 @@ public class AtTests extends Common {
public void testCompile() {
Account deployer = Common.getTestAccount(null, "chloe");
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@ -113,7 +113,7 @@ public class AtTests extends Common {
@SuppressWarnings("unused")
@Test
public void testAutomaticOfferRefund() throws DataException {
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
@ -128,15 +128,29 @@ public class AtTests extends Common {
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
// Send creator's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
BigDecimal messageFee = messageTransaction.getTransactionData().getFee();
// Refund should happen 1st block after receiving recipient address
BlockUtils.mintBlock(repository);
BigDecimal expectedMinimumBalance = deployersPostDeploymentBalance;
BigDecimal expectedMaximumBalance = deployersInitialBalance.subtract(deployAtFee).subtract(messageFee);
BigDecimal actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance.toPlainString(), expectedMinimumBalance.toPlainString()), actualBalance.compareTo(expectedMinimumBalance) > 0);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance.toPlainString(), expectedMaximumBalance.toPlainString()), actualBalance.compareTo(expectedMaximumBalance) < 0);
describeAt(repository, atAddress);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
BigDecimal expectedBalance = deployersPostDeploymentBalance;
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
BigDecimal expectedBalance = deployersPostDeploymentBalance.subtract(messageFee);
actualBalance = deployer.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
@ -237,7 +251,7 @@ public class AtTests extends Common {
BigDecimal messageFee = messageTransaction.getTransactionData().getFee();
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee).subtract(messageFee);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
@ -340,7 +354,7 @@ public class AtTests extends Common {
describeAt(repository, atAddress);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@ -382,7 +396,7 @@ public class AtTests extends Common {
describeAt(repository, atAddress);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@ -421,7 +435,7 @@ public class AtTests extends Common {
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
@ -475,7 +489,7 @@ public class AtTests extends Common {
return messageTransaction;
}
private void checkAtRefund(Repository repository, Account deployer, BigDecimal deployersInitialBalance, BigDecimal deployAtFee) throws DataException {
private void checkTradeRefund(Repository repository, Account deployer, BigDecimal deployersInitialBalance, BigDecimal deployAtFee) throws DataException {
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee);
// AT should automatically refund deployer after 'refundTimeout' blocks
@ -520,7 +534,6 @@ public class AtTests extends Common {
+ "\tinitial payout: %s QORT,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n"
+ "\toffer timeout: %d minutes (from creation),\n"
+ "\ttrade timeout: %d minutes (from trade start),\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAddress,
@ -531,18 +544,17 @@ public class AtTests extends Common {
tradeData.initialPayout.toPlainString(),
tradeData.redeemPayout.toPlainString(),
tradeData.expectedBitcoin.toPlainString(),
tradeData.offerRefundTimeout,
tradeData.tradeRefundTimeout,
currentBlockHeight));
// Are we in 'offer' or 'trade' stage?
if (tradeData.tradeRefundHeight == null) {
// Offer
System.out.println(String.format("\toffer timeout: block %d",
tradeData.offerRefundHeight));
System.out.println(String.format("\tstatus: 'offer mode'"));
} else {
// Trade
System.out.println(String.format("\ttrade timeout: block %d,\n"
System.out.println(String.format("\tstatus: 'trade mode',\n"
+ "\ttrade timeout: block %d,\n"
+ "\ttrade recipient: %s",
tradeData.tradeRefundHeight,
tradeData.qortalRecipient));

View File

@ -54,7 +54,7 @@ public class BuildP2SH {
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {
@ -74,8 +74,8 @@ public class BuildP2SH {
lockTime = Integer.parseInt(args[argIndex++]);
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now");
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);

View File

@ -58,7 +58,7 @@ public class CheckP2SH {
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {

View File

@ -0,0 +1,9 @@
package org.qortal.test.btcacct;
import org.bitcoinj.core.Coin;
public abstract class Common {
public static final Coin DEFAULT_BTC_FEE = Coin.parseCoin("0.00001000");
}

View File

@ -34,19 +34,20 @@ 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> <HASH160-of-secret> [<initial QORT payout> [<AT funding amount>]]"));
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n"
+ "\t0.00864200 \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t0.0001 \\\n"
+ "\t123.456"));
+ "\t123.456 \\\n"
+ "\t10"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 5 || args.length > 7)
if (args.length != 8)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
@ -58,6 +59,7 @@ public class DeployAT {
byte[] secretHash = null;
BigDecimal initialPayout = BigDecimal.ZERO.setScale(8);
BigDecimal fundingAmount = null;
int tradeTimeout = 0;
int argIndex = 0;
try {
@ -77,15 +79,15 @@ public class DeployAT {
if (secretHash.length != 20)
usage("Hash of secret must be 20 bytes");
if (args.length > argIndex)
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
if (args.length > argIndex) {
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8);
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8);
if (fundingAmount.compareTo(redeemAmount) <= 0)
usage("AT funding amount must be greater than QORT redeem amount");
if (fundingAmount.compareTo(redeemAmount) <= 0)
usage("AT funding amount must be greater than QORT redeem amount");
}
tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 10 || tradeTimeout > 50000)
usage("AT trade timeout should be between 10 and 50,000 minutes");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
@ -105,17 +107,12 @@ public class DeployAT {
System.out.println(String.format("QORT redeem amount: %s", redeemAmount.toPlainString()));
if (fundingAmount == null)
fundingAmount = redeemAmount.add(atFundingExtra);
System.out.println(String.format("AT funding amount: %s", fundingAmount.toPlainString()));
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT
final int offerTimeout = 2 * 60; // minutes
final int tradeTimeout = 60; // minutes
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, offerTimeout, tradeTimeout, initialPayout, fundingAmount, expectedBitcoin);
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis();
@ -133,7 +130,7 @@ public class DeployAT {
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, redeemAmount, Asset.QORT);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);

View File

@ -64,7 +64,7 @@ public class Redeem {
byte[] redeemPrivateKey = null;
byte[] secret = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {

View File

@ -64,7 +64,7 @@ public class Refund {
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {