diff --git a/src/main/java/org/qortal/api/resource/ApiDefinition.java b/src/main/java/org/qortal/api/resource/ApiDefinition.java index 52b6ade0..c887d4bc 100644 --- a/src/main/java/org/qortal/api/resource/ApiDefinition.java +++ b/src/main/java/org/qortal/api/resource/ApiDefinition.java @@ -13,7 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "Admin"), @Tag(name = "Arbitrary"), @Tag(name = "Assets"), + @Tag(name = "Automated Transactions"), @Tag(name = "Blocks"), + @Tag(name = "Cross-Chain"), @Tag(name = "Groups"), @Tag(name = "Names"), @Tag(name = "Payments"), diff --git a/src/main/java/org/qortal/api/resource/AtResource.java b/src/main/java/org/qortal/api/resource/AtResource.java new file mode 100644 index 00000000..86b79da9 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/AtResource.java @@ -0,0 +1,206 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +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.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +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.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiException; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.at.QortalAtLoggerFactory; +import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; +import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.ValidationResult; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.DeployAtTransactionTransformer; +import org.qortal.utils.Base58; + +@Path("/at") +@Tag(name = "Automated Transactions") +public class AtResource { + + @Context + HttpServletRequest request; + + @GET + @Path("/byfunction/{codehash}") + @Operation( + summary = "Find automated transactions with matching functionality (code hash)", + responses = { + @ApiResponse( + description = "automated transactions", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = ATData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE + }) + public List getByFunctionality( + @PathParam("codehash") + String codeHash58, + @Parameter(description = "whether to include ATs that can run, or not, or both (if omitted)") + @QueryParam("isExecutable") + Boolean isExecutable, + @Parameter( ref = "limit") @QueryParam("limit") Integer limit, + @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, + @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { + // Decode codeHash + byte[] codeHash; + try { + codeHash = Base58.decode(codeHash58); + } catch (NumberFormatException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e); + } + + // codeHash must be present and have correct length + if (codeHash == null || codeHash.length != 32) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + // Impose a limit on 'limit' + if (limit != null && limit > 100) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{ataddress}") + @Operation( + summary = "Fetch info associated with AT address", + responses = { + @ApiResponse( + description = "automated transaction", + content = @Content( + schema = @Schema(implementation = ATData.class) + ) + ) + } + ) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) + public ATData getByAddress(@PathParam("ataddress") String atAddress) { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getATRepository().fromATAddress(atAddress); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{ataddress}/data") + @Operation( + summary = "Fetch data segment associated with AT address", + responses = { + @ApiResponse( + description = "automated transaction", + content = @Content( + schema = @Schema(implementation = byte[].class) + ) + ) + } + ) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) + public byte[] getDataByAddress(@PathParam("ataddress") String atAddress) { + try (final Repository repository = RepositoryManager.getRepository()) { + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + return dataBytes; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Operation( + summary = "Build raw, unsigned, DEPLOY_AT transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = DeployAtTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, DEPLOY_AT transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String createDeployAt(DeployAtTransactionData transactionData) { + if (Settings.getInstance().isApiRestricted()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); + + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = DeployAtTransactionTransformer.toBytes(transactionData); + 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); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java new file mode 100644 index 00000000..83dbb0f8 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -0,0 +1,105 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; + +import org.ciyam.at.MachineState; +import org.qortal.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiException; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.asset.Asset; +import org.qortal.at.QortalAtLoggerFactory; +import org.qortal.crosschain.BTCACCT; +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.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; + +@Path("/crosschain") +@Tag(name = "Cross-Chain") +public class CrossChainResource { + + @Context + HttpServletRequest request; + + @GET + @Path("/tradeoffers") + @Operation( + summary = "Find cross-chain trade offers", + responses = { + @ApiResponse( + description = "automated transactions", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = CrossChainTradeData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE + }) + public List getTradeOffers( + @Parameter( ref = "limit") @QueryParam("limit") Integer limit, + @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, + @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { + // Impose a limit on 'limit' + if (limit != null && limit > 100) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + byte[] codeHash = BTCACCT.CODE_BYTES_HASH; + boolean isExecutable = true; + + try (final Repository repository = RepositoryManager.getRepository()) { + List atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse); + + List crossChainTradesData = new ArrayList<>(); + for (ATData atData : atsData) { + String atAddress = atData.getATAddress(); + + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, atStateData.getStateData()); + + CrossChainTradeData crossChainTradeData = new CrossChainTradeData(); + crossChainTradeData.qortalAddress = atAddress; + crossChainTradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey()); + crossChainTradeData.creationTimestamp = atData.getCreation(); + crossChainTradeData.qortBalance = repository.getAccountRepository().getBalance(atAddress, Asset.QORT).getBalance(); + + BTCACCT.populateTradeData(crossChainTradeData, dataBytes); + + crossChainTradesData.add(crossChainTradeData); + } + + return crossChainTradesData; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java index 1ba5d8e2..d9c0adcd 100644 --- a/src/main/java/org/qortal/at/AT.java +++ b/src/main/java/org/qortal/at/AT.java @@ -58,7 +58,9 @@ public class AT { MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes()); - this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), + byte[] codeHash = Crypto.digest(machineState.getCodeBytes()); + + this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash, machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(), machineState.isFrozen(), machineState.getFrozenBalance()); @@ -104,9 +106,10 @@ public class AT { boolean hadFatalError = false; boolean isFrozen = false; Long frozenBalance = null; + byte[] codeHash = Crypto.digest(codeBytes); - this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, isSleeping, sleepUntilHeight, isFinished, - hadFatalError, isFrozen, frozenBalance); + this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8), true); } diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 9455e59e..ea3f67dd 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -348,10 +348,15 @@ public class QortalATAPI extends API { @Override public void putCreatorAddressIntoB(MachineState state) { - // Simply use raw public key byte[] publicKey = atData.getCreatorPublicKey(); + String address = Crypto.toAddress(publicKey); - this.setB(state, publicKey); + // Convert to byte form as this only takes 25 bytes, + // compared to string-form's 34 bytes, + // and we only have 32 bytes available. + byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes + + this.setB(state, addressBytes); } @Override diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 2789fb3d..c89eb456 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -24,7 +24,9 @@ import org.ciyam.at.CompilationException; import org.ciyam.at.FunctionCode; import org.ciyam.at.MachineState; import org.ciyam.at.OpCode; +import org.ciyam.at.Timestamp; import org.qortal.account.Account; +import org.qortal.data.crosschain.CrossChainTradeData; import org.qortal.utils.Base58; import org.qortal.utils.BitTwiddling; @@ -34,23 +36,7 @@ import com.google.common.primitives.Bytes; 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("750012c7ae79d85a97e64e94c467c7791dd76cf3050b864f3166635a21d767c6").asBytes(); // SHA256 of AT code bytes - - public static class AtConstants { - public final byte[] secretHash; - public final BigDecimal initialPayout; - public final BigDecimal redeemPayout; - public final String recipient; - public final int refundMinutes; - - public AtConstants(byte[] secretHash, BigDecimal initialPayout, BigDecimal redeemPayout, String recipient, int refundMinutes) { - this.secretHash = secretHash; - this.initialPayout = initialPayout; - this.redeemPayout = redeemPayout; - this.recipient = recipient; - this.refundMinutes = refundMinutes; - } - } + public static final byte[] CODE_BYTES_HASH = HashCode.fromString("da7271e9aa697112ece632cf2b462fded74843944a704b9d5fd4ae5971f6686f").asBytes(); // SHA256 of AT code bytes /* * OP_TUCK (to copy public key to before signature) @@ -194,59 +180,98 @@ public class BTCACCT { return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder); } - @SuppressWarnings("unused") - public static byte[] buildQortalAT(byte[] secretHash, String recipientQortalAddress, int refundMinutes, BigDecimal initialPayout, BigDecimal redeemPayout) { + /* + * 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 static byte[] buildQortalAT(String qortalCreator, byte[] secretHash, int offerTimeout, int tradeTimeout, BigDecimal initialPayout, BigDecimal redeemPayout, BigDecimal bitcoinAmount) { // Labels for data segment addresses int addrCounter = 0; + // Constants (with corresponding dataByteBuffer.put*() calls below) - final int addrHashPart1 = addrCounter++; - final int addrHashPart2 = addrCounter++; - final int addrHashPart3 = addrCounter++; - final int addrHashPart4 = addrCounter++; - final int addrAddressPart1 = addrCounter++; - final int addrAddressPart2 = addrCounter++; - final int addrAddressPart3 = addrCounter++; - final int addrAddressPart4 = addrCounter++; - final int addrRefundMinutes = addrCounter++; + + final int addrQortalCreator1 = addrCounter++; + final int addrQortalCreator2 = addrCounter++; + final int addrQortalCreator3 = addrCounter++; + final int addrQortalCreator4 = addrCounter++; + + final int addrSecretHash = addrCounter; + addrCounter += 4; + + final int addrOfferTimeout = addrCounter++; + final int addrTradeTimeout = addrCounter++; final int addrInitialPayoutAmount = addrCounter++; final int addrRedeemPayoutAmount = addrCounter++; - final int addrExpectedTxType = addrCounter++; - final int addrHashIndex = addrCounter++; - final int addrAddressIndex = addrCounter++; - final int addrAddressTempIndex = addrCounter++; - final int addrHashTempIndex = addrCounter++; - final int addrHashTempLength = addrCounter++; + final int addrBitcoinAmount = addrCounter++; + + final int addrMessageTxType = addrCounter++; + + final int addrSecretHashPointer = addrCounter++; + final int addrQortalRecipientPointer = addrCounter++; + final int addrMessageSenderPointer = addrCounter++; + + final int addrMessageDataPointer = addrCounter++; + final int addrMessageDataLength = addrCounter++; + final int addrEndOfConstants = addrCounter; + // Variables - final int addrRefundTimestamp = addrCounter++; - final int addrLastTimestamp = addrCounter++; + + final int addrQortalRecipient1 = addrCounter++; + final int addrQortalRecipient2 = addrCounter++; + final int addrQortalRecipient3 = addrCounter++; + final int addrQortalRecipient4 = addrCounter++; + + final int addrOfferRefundTimestamp = addrCounter++; + final int addrTradeRefundTimestamp = addrCounter++; + final int addrLastTxTimestamp = addrCounter++; final int addrBlockTimestamp = addrCounter++; final int addrTxType = addrCounter++; - final int addrComparator = addrCounter++; - final int addrAddressTemp1 = addrCounter++; - final int addrAddressTemp2 = addrCounter++; - final int addrAddressTemp3 = addrCounter++; - final int addrAddressTemp4 = addrCounter++; - final int addrHashTemp1 = addrCounter++; - final int addrHashTemp2 = addrCounter++; - final int addrHashTemp3 = addrCounter++; - final int addrHashTemp4 = addrCounter++; + final int addrResult = addrCounter++; + + final int addrMessageSender1 = addrCounter++; + final int addrMessageSender2 = addrCounter++; + final int addrMessageSender3 = addrCounter++; + final int addrMessageSender4 = addrCounter++; + + final int addrMessageData = addrCounter; + addrCounter += 4; // Data segment ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); - // Hash of secret into HashPart1-4 - assert dataByteBuffer.position() == addrHashPart1 * MachineState.VALUE_SIZE : "addrHashPart1 incorrect"; + // AT creator's Qortal address, decoded from Base58 + assert dataByteBuffer.position() == addrQortalCreator1 * MachineState.VALUE_SIZE : "addrQortalCreator1 incorrect"; + byte[] qortalCreatorBytes = Base58.decode(qortalCreator); + dataByteBuffer.put(Bytes.ensureCapacity(qortalCreatorBytes, 32, 0)); + + // Hash of secret + assert dataByteBuffer.position() == addrSecretHash * MachineState.VALUE_SIZE : "addrSecretHash incorrect"; dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0)); - // Recipient Qortal address, decoded from Base58 - assert dataByteBuffer.position() == addrAddressPart1 * MachineState.VALUE_SIZE : "addrAddressPart1 incorrect"; - byte[] recipientAddressBytes = Base58.decode(recipientQortalAddress); - dataByteBuffer.put(Bytes.ensureCapacity(recipientAddressBytes, 32, 0)); + // Open offer timeout in minutes + assert dataByteBuffer.position() == addrOfferTimeout * MachineState.VALUE_SIZE : "addrOfferTimeout incorrect"; + dataByteBuffer.putLong(offerTimeout); - // Expiry in minutes - assert dataByteBuffer.position() == addrRefundMinutes * MachineState.VALUE_SIZE : "addrRefundMinutes incorrect"; - dataByteBuffer.putLong(refundMinutes); + // Trade timeout in minutes + assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; + dataByteBuffer.putLong(tradeTimeout); // Initial payout amount assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect"; @@ -256,34 +281,42 @@ public class BTCACCT { assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect"; dataByteBuffer.putLong(redeemPayout.unscaledValue().longValue()); + // Expected Bitcoin amount + assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect"; + dataByteBuffer.putLong(bitcoinAmount.unscaledValue().longValue()); + // We're only interested in MESSAGE transactions - assert dataByteBuffer.position() == addrExpectedTxType * MachineState.VALUE_SIZE : "addrExpectedTxType incorrect"; + assert dataByteBuffer.position() == addrMessageTxType * MachineState.VALUE_SIZE : "addrMessageTxType incorrect"; dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value); // Index into data segment of hash, used by GET_B_IND - assert dataByteBuffer.position() == addrHashIndex * MachineState.VALUE_SIZE : "addrHashIndex incorrect"; - dataByteBuffer.putLong(addrHashPart1); + assert dataByteBuffer.position() == addrSecretHashPointer * MachineState.VALUE_SIZE : "addrSecretHashPointer incorrect"; + dataByteBuffer.putLong(addrSecretHash); // Index into data segment of recipient address, used by SET_B_IND - assert dataByteBuffer.position() == addrAddressIndex * MachineState.VALUE_SIZE : "addrAddressIndex incorrect"; - dataByteBuffer.putLong(addrAddressPart1); + assert dataByteBuffer.position() == addrQortalRecipientPointer * MachineState.VALUE_SIZE : "addrQortalRecipientPointer incorrect"; + dataByteBuffer.putLong(addrQortalRecipient1); // Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND - assert dataByteBuffer.position() == addrAddressTempIndex * MachineState.VALUE_SIZE : "addrAddressTempIndex incorrect"; - dataByteBuffer.putLong(addrAddressTemp1); + assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect"; + dataByteBuffer.putLong(addrMessageSender1); // Source location and length for hashing any passed secret - assert dataByteBuffer.position() == addrHashTempIndex * MachineState.VALUE_SIZE : "addrHashTempIndex incorrect"; - dataByteBuffer.putLong(addrHashTemp1); - assert dataByteBuffer.position() == addrHashTempLength * MachineState.VALUE_SIZE : "addrHashTempLength incorrect"; + assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect"; + dataByteBuffer.putLong(addrMessageData); + assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect"; dataByteBuffer.putLong(32L); assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants"; // Code labels - Integer labelTxLoop = null; Integer labelRefund = null; - Integer labelCheckTx = null; + + Integer labelOfferTxLoop = null; + Integer labelCheckOfferTx = null; + + Integer labelTradeTxLoop = null; + Integer labelCheckTradeTx = null; ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); @@ -295,83 +328,136 @@ 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, addrLastTimestamp)); - // Calculate refund 'timestamp' by adding minutes to above 'timestamp', then save into addrRefundTimestamp - codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTimestamp, addrRefundMinutes)); + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp)); - // Load recipient's address into B register - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressIndex)); - // 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)); + // 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()); - /* Main loop */ + /* 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 refund 'timestamp' then look for next transaction - codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelTxLoop))); - // We're past refund 'timestamp' so go refund everything back to AT creator + // 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)); /* Transaction processing loop */ - labelTxLoop = codeByteBuffer.position(); + labelOfferTxLoop = 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, addrLastTimestamp)); + 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. - codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrComparator)); - // If addrComparator is zero (i.e. A is non-zero, transaction was found) then go check transaction - codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelCheckTx))); + 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))); // Stop and wait for next block codeByteBuffer.put(OpCode.STP_IMD.compile()); /* Check transaction */ - labelCheckTx = codeByteBuffer.position(); + labelCheckOfferTx = 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, addrLastTimestamp)); + 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)); // If transaction type is not MESSAGE type then go look for another transaction - codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrExpectedTxType, calcOffset(codeByteBuffer, labelTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelOfferTxLoop))); /* Check transaction's sender */ // 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 addrAddressTemp1 (as pointed to by addrAddressTempIndex) - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrAddressTempIndex)); + // 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(addrAddressTemp1, addrAddressPart1, calcOffset(codeByteBuffer, labelTxLoop))); - codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp2, addrAddressPart2, calcOffset(codeByteBuffer, labelTxLoop))); - codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp3, addrAddressPart3, calcOffset(codeByteBuffer, labelTxLoop))); - codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp4, addrAddressPart4, calcOffset(codeByteBuffer, labelTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalCreator1, calcOffset(codeByteBuffer, labelOfferTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalCreator2, calcOffset(codeByteBuffer, labelOfferTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalCreator3, calcOffset(codeByteBuffer, labelOfferTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalCreator4, calcOffset(codeByteBuffer, labelOfferTxLoop))); + + /* 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)); + // 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)); + + // Calculate trade timeout refund 'timestamp' by adding addrTradeTimeout minutes to above message's 'timestamp', then save into addrTradeRefundTimestamp + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrTradeRefundTimestamp, addrLastTxTimestamp, addrTradeTimeout)); + + // 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 */ + + // 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, addrTradeRefundTimestamp, calcOffset(codeByteBuffer, labelTradeTxLoop))); + // 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(); + + // 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. + 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))); + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_IMD.compile()); + + /* Check transaction */ + labelCheckTradeTx = 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)); + // If transaction type is not MESSAGE type then go look for another transaction + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelTradeTxLoop))); + + /* Check transaction's sender */ + + // 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))); /* Check 'secret' in transaction's 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 addrHashTemp1 (as pointed to by addrHashTempIndex) - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashTempIndex)); - // Load B register with expected hash result (as pointed to by addrHashIndex) - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashIndex)); - // Perform HASH160 using source data at addrHashTemp1 through addrHashTemp4. (Location and length specified via addrHashTempIndex and addrHashTemplength). - // Save the equality result (1 if they match, 0 otherwise) into addrComparator. - codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrComparator, addrHashTempIndex, addrHashTempLength)); - // If hashes don't match, addrComparator will be zero so go find another transaction - codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelTxLoop))); + // 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 addrSecretHashPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrSecretHashPointer)); + // Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength). + // Save the equality result (1 if they match, 0 otherwise) into addrResult. + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength)); + // If hashes don't match, addrResult will be zero so go find another transaction + codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelTradeTxLoop))); /* Success! Pay arranged amount to intended recipient */ - // Load B register with intended recipient address (as pointed to by addrAddressIndex) - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressIndex)); + // Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer)); // Pay AT's balance to recipient codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount)); - // We're finished forever - 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(); @@ -400,23 +486,63 @@ public class BTCACCT { return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); } - public static AtConstants extractAtConstants(byte[] dataBytes) { + public static void populateTradeData(CrossChainTradeData tradeData, byte[] dataBytes) { ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes); - - byte[] secretHash = new byte[32]; - dataByteBuffer.get(secretHash); - byte[] addressBytes = new byte[32]; + + // Skip AT creator address + dataByteBuffer.position(dataByteBuffer.position() + 32); + + // Hash of secret + tradeData.secretHash = new byte[32]; + dataByteBuffer.get(tradeData.secretHash); + + // Offer timeout + tradeData.offerRefundTimeout = dataByteBuffer.getLong(); + + // Trade timeout + tradeData.tradeRefundTimeout = dataByteBuffer.getLong(); + + // Initial payout + tradeData.initialPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8); + + // Redeem payout + tradeData.redeemPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8); + + // Expected BTC amount + tradeData.expectedBitcoin = BigDecimal.valueOf(dataByteBuffer.getLong(), 8); + + // Skip MESSAGE transaction type + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to secretHash + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to Qortal recipient + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to message sender + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to message data + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip message data length + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Qortal recipient (if any) dataByteBuffer.get(addressBytes); - String recipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH)); + if (addressBytes[0] != 0) + tradeData.qortalRecipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH)); - int refundMinutes = (int) dataByteBuffer.getLong(); + // Open offer timeout (AT 'timestamp' converted to Qortal block height) + long offerRefundTimestamp = dataByteBuffer.getLong(); + tradeData.offerRefundHeight = new Timestamp(offerRefundTimestamp).blockHeight; - BigDecimal initialPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8); - - BigDecimal redeemPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8); - - return new AtConstants(secretHash, initialPayout, redeemPayout, recipient, refundMinutes); + // Trade offer timeout (AT 'timestamp' converted to Qortal block height) + long tradeRefundTimestamp = dataByteBuffer.getLong(); + if (tradeRefundTimestamp != 0) + tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; } } diff --git a/src/main/java/org/qortal/data/at/ATData.java b/src/main/java/org/qortal/data/at/ATData.java index b12439e0..2e2a0c49 100644 --- a/src/main/java/org/qortal/data/at/ATData.java +++ b/src/main/java/org/qortal/data/at/ATData.java @@ -2,6 +2,11 @@ package org.qortal.data.at; import java.math.BigDecimal; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +// All properties to be converted to JSON via JAX-RS +@XmlAccessorType(XmlAccessType.FIELD) public class ATData { // Properties @@ -11,6 +16,7 @@ public class ATData { private int version; private long assetId; private byte[] codeBytes; + private byte[] codeHash; private boolean isSleeping; private Integer sleepUntilHeight; private boolean isFinished; @@ -20,14 +26,19 @@ public class ATData { // Constructors - public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping, - Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) { + // necessary for JAX-RS serialization + protected ATData() { + } + + public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash, + boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) { this.ATAddress = ATAddress; this.creatorPublicKey = creatorPublicKey; this.creation = creation; this.version = version; this.assetId = assetId; this.codeBytes = codeBytes; + this.codeHash = codeHash; this.isSleeping = isSleeping; this.sleepUntilHeight = sleepUntilHeight; this.isFinished = isFinished; @@ -36,10 +47,10 @@ public class ATData { this.frozenBalance = frozenBalance; } - public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping, - Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) { - this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, - (BigDecimal) null); + public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash, + boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) { + this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null); // Convert Long frozenBalance to BigDecimal if (frozenBalance != null) @@ -80,6 +91,10 @@ public class ATData { return this.codeBytes; } + public byte[] getCodeHash() { + return this.codeHash; + } + public boolean getIsSleeping() { return this.isSleeping; } diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java new file mode 100644 index 00000000..d055c830 --- /dev/null +++ b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java @@ -0,0 +1,61 @@ +package org.qortal.data.crosschain; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainTradeData { + + // Properties + + @Schema(description = "AT's Qortal address") + public String qortalAddress; + + @Schema(description = "AT creator's Qortal address") + public String qortalCreator; + + @Schema(description = "Timestamp when AT was created (milliseconds since epoch)") + public long creationTimestamp; + + @Schema(description = "AT's current QORT balance") + public BigDecimal qortBalance; + + @Schema(description = "HASH160 of 32-byte secret") + public byte[] secretHash; + + @Schema(description = "Initial QORT payment that will be sent to Qortal trade partner") + public BigDecimal initialPayout; + + @Schema(description = "Final QORT payment that will be sent to Qortal trade partner") + public BigDecimal redeemPayout; + + @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; + + @Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)") + public Integer tradeRefundHeight; + + @Schema(description = "Amount, in BTC, that AT creator expects Bitcoin P2SH to pay out (excluding miner fees)") + public BigDecimal expectedBitcoin; + + // Constructors + + // Necessary for JAXB + public CrossChainTradeData() { + } + +} diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java index 9653fc6c..affbaf18 100644 --- a/src/main/java/org/qortal/repository/ATRepository.java +++ b/src/main/java/org/qortal/repository/ATRepository.java @@ -18,6 +18,9 @@ public interface ATRepository { /** Returns list of executable ATs, empty if none found */ public List getAllExecutableATs() throws DataException; + /** Returns list of ATs with matching code hash, optionally executable only. */ + public List getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** Returns creation block height given AT's address or null if not found */ public Integer getATCreationBlockHeight(String atAddress) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index dd85a241..e161437a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -26,7 +26,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATData fromATAddress(String atAddress) throws DataException { - String sql = "SELECT creator, creation, version, asset_id, code_bytes, " + String sql = "SELECT creator, creation, version, asset_id, code_bytes, code_hash, " + "is_sleeping, sleep_until_height, is_finished, had_fatal_error, " + "is_frozen, frozen_balance " + "FROM ATs " @@ -41,20 +41,21 @@ public class HSQLDBATRepository implements ATRepository { int version = resultSet.getInt(3); long assetId = resultSet.getLong(4); byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(6); + byte[] codeHash = resultSet.getBytes(6); + boolean isSleeping = resultSet.getBoolean(7); - Integer sleepUntilHeight = resultSet.getInt(7); + Integer sleepUntilHeight = resultSet.getInt(8); if (sleepUntilHeight == 0 && resultSet.wasNull()) sleepUntilHeight = null; - boolean isFinished = resultSet.getBoolean(8); - boolean hadFatalError = resultSet.getBoolean(9); - boolean isFrozen = resultSet.getBoolean(10); + boolean isFinished = resultSet.getBoolean(9); + boolean hadFatalError = resultSet.getBoolean(10); + boolean isFrozen = resultSet.getBoolean(11); - BigDecimal frozenBalance = resultSet.getBigDecimal(11); + BigDecimal frozenBalance = resultSet.getBigDecimal(12); - return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, - isFrozen, frozenBalance); + return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); } @@ -71,7 +72,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getAllExecutableATs() throws DataException { - String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, " + String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, code_hash, " + "is_sleeping, sleep_until_height, had_fatal_error, " + "is_frozen, frozen_balance " + "FROM ATs " @@ -93,19 +94,20 @@ public class HSQLDBATRepository implements ATRepository { int version = resultSet.getInt(4); long assetId = resultSet.getLong(5); byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(7); + byte[] codeHash = resultSet.getBytes(7); + boolean isSleeping = resultSet.getBoolean(8); - Integer sleepUntilHeight = resultSet.getInt(8); + Integer sleepUntilHeight = resultSet.getInt(9); if (sleepUntilHeight == 0 && resultSet.wasNull()) sleepUntilHeight = null; - boolean hadFatalError = resultSet.getBoolean(9); - boolean isFrozen = resultSet.getBoolean(10); + boolean hadFatalError = resultSet.getBoolean(10); + boolean isFrozen = resultSet.getBoolean(11); - BigDecimal frozenBalance = resultSet.getBigDecimal(11); + BigDecimal frozenBalance = resultSet.getBigDecimal(12); - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, - hadFatalError, isFrozen, frozenBalance); + ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); executableATs.add(atData); } while (resultSet.next()); @@ -116,6 +118,62 @@ public class HSQLDBATRepository implements ATRepository { } } + @Override + public List getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException { + StringBuilder sql = new StringBuilder(512); + sql.append("SELECT AT_address, creator, creation, version, asset_id, code_bytes, ") + .append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ") + .append("is_frozen, frozen_balance ") + .append("FROM ATs ") + .append("WHERE code_hash = ? "); + + if (isExecutable != null) + sql.append("AND is_finished = ").append(isExecutable ? "false" : "true"); + + sql.append(" ORDER BY creation "); + if (reverse != null && reverse) + sql.append("DESC"); + + HSQLDBRepository.limitOffsetSql(sql, limit, offset); + + List matchingATs = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), codeHash)) { + if (resultSet == null) + return matchingATs; + + do { + String atAddress = resultSet.getString(1); + byte[] creatorPublicKey = resultSet.getBytes(2); + long creation = getZonedTimestampMilli(resultSet, 3); + int version = resultSet.getInt(4); + long assetId = resultSet.getLong(5); + byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB + boolean isSleeping = resultSet.getBoolean(7); + + Integer sleepUntilHeight = resultSet.getInt(8); + if (sleepUntilHeight == 0 && resultSet.wasNull()) + sleepUntilHeight = null; + + boolean isFinished = resultSet.getBoolean(9); + + boolean hadFatalError = resultSet.getBoolean(10); + boolean isFrozen = resultSet.getBoolean(11); + + BigDecimal frozenBalance = resultSet.getBigDecimal(12); + + ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); + + matchingATs.add(atData); + } while (resultSet.next()); + + return matchingATs; + } catch (SQLException e) { + throw new DataException("Unable to fetch matching ATs from repository", e); + } + } + @Override public Integer getATCreationBlockHeight(String atAddress) throws DataException { String sql = "SELECT height " @@ -140,7 +198,8 @@ public class HSQLDBATRepository implements ATRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(atData.getCreation())) - .bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()).bind("code_bytes", atData.getCodeBytes()) + .bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()) + .bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash()) .bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight()) .bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()) .bind("frozen_balance", atData.getFrozenBalance()); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 562f5030..c279c22a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -980,6 +980,11 @@ public class HSQLDBDatabaseUpdates { stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE"); break; + case 72: + // For ATs, add hash of code bytes to allow searching for specific function ATs, e.g. cross-chain trading + stmt.execute("ALTER TABLE ATs ADD COLUMN code_hash VARBINARY(32) NOT NULL BEFORE is_sleeping"); // Assuming something like SHA256 + break; + default: // nothing to do return false; diff --git a/src/test/java/org/qortal/test/btcacct/AtTests.java b/src/test/java/org/qortal/test/btcacct/AtTests.java index 67968af6..21f4166c 100644 --- a/src/test/java/org/qortal/test/btcacct/AtTests.java +++ b/src/test/java/org/qortal/test/btcacct/AtTests.java @@ -10,7 +10,9 @@ import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.Arrays; import java.util.List; +import java.util.function.Function; +import org.bitcoinj.core.Base58; import org.ciyam.at.MachineState; import org.junit.Before; import org.junit.Test; @@ -22,6 +24,7 @@ import org.qortal.crosschain.BTCACCT; 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; @@ -37,6 +40,7 @@ import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.MessageTransaction; import com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; public class AtTests extends Common { @@ -46,6 +50,7 @@ public class AtTests extends Common { public static final BigDecimal initialPayout = new BigDecimal("0.001").setScale(8); public static final BigDecimal redeemAmount = new BigDecimal("80.4020").setScale(8); public static final BigDecimal fundingAmount = new BigDecimal("123.456").setScale(8); + public static final BigDecimal bitcoinAmount = new BigDecimal("0.00864200").setScale(8); @Before public void beforeTest() throws DataException { @@ -54,9 +59,9 @@ public class AtTests extends Common { @Test public void testCompile() { - String redeemAddress = Common.getTestAccount(null, "chloe").getAddress(); + Account deployer = Common.getTestAccount(null, "chloe"); - byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout, redeemAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); } @@ -69,7 +74,7 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); BigDecimal expectedBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtTransaction.getTransactionData().getFee()); BigDecimal actualBalance = deployer.getBalance(Asset.QORT); @@ -106,6 +111,37 @@ public class AtTests extends Common { } } + @SuppressWarnings("unused") + @Test + public void testAutomaticOfferRefund() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); + + BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); + BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee(); + BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee); + + checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee); + + describeAt(repository, atAddress); + + // Test orphaning + BlockUtils.orphanLastBlock(repository); + + BigDecimal expectedBalance = deployersPostDeploymentBalance; + BigDecimal actualBalance = deployer.getBalance(Asset.QORT); + + Common.assertEqualBigDecimals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); + } + } + @SuppressWarnings("unused") @Test public void testInitialPayment() throws DataException { @@ -116,9 +152,15 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); - // Initial payment should happen 1st block after deployment + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address BlockUtils.mintBlock(repository); BigDecimal expectedBalance = recipientsInitialBalance.add(initialPayout); @@ -126,6 +168,8 @@ public class AtTests extends Common { Common.assertEqualBigDecimals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance); + describeAt(repository, atAddress); + // Test orphaning BlockUtils.orphanLastBlock(repository); @@ -136,9 +180,41 @@ public class AtTests extends Common { } } + // TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @SuppressWarnings("unused") @Test - public void testAutomaticRefund() throws DataException { + public void testIncorrectTradeSender() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); + PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); + + BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); + BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT BUT NOT FROM AT CREATOR + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress); + + // Initial payment should NOT happen + BlockUtils.mintBlock(repository); + + BigDecimal expectedBalance = recipientsInitialBalance; + BigDecimal actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + Common.assertEqualBigDecimals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance); + + describeAt(repository, atAddress); + } + } + + @SuppressWarnings("unused") + @Test + public void testAutomaticTradeRefund() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); @@ -146,15 +222,28 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee(); - BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee); + BigDecimal messageFee = messageTransaction.getTransactionData().getFee(); + BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee).subtract(messageFee); checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee); + describeAt(repository, atAddress); + // Test orphaning BlockUtils.orphanLastBlock(repository); + BlockUtils.orphanLastBlock(repository); BigDecimal expectedBalance = deployersPostDeploymentBalance; BigDecimal actualBalance = deployer.getBalance(Asset.QORT); @@ -173,12 +262,19 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + // Send correct secret to AT - MessageTransaction messageTransaction = sendSecret(repository, recipient, secret, atAddress); + messageTransaction = sendMessage(repository, recipient, secret, atAddress); // AT should send funds in the next block ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); @@ -189,6 +285,8 @@ public class AtTests extends Common { Common.assertEqualBigDecimals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance); + describeAt(repository, atAddress); + // Orphan redeem BlockUtils.orphanLastBlock(repository); @@ -215,14 +313,21 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee(); Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + // Send correct secret to AT, but from wrong account - MessageTransaction messageTransaction = sendSecret(repository, bystander, secret, atAddress); + messageTransaction = sendMessage(repository, bystander, secret, atAddress); // AT should NOT send funds in the next block ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); @@ -233,6 +338,8 @@ public class AtTests extends Common { Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance); + describeAt(repository, atAddress); + checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee); } } @@ -247,15 +354,22 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee(); Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + // Send correct secret to AT, but from wrong account byte[] wrongSecret = Crypto.digest(secret); - MessageTransaction messageTransaction = sendSecret(repository, recipient, wrongSecret, atAddress); + messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress); // AT should NOT send funds in the next block ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); @@ -266,6 +380,8 @@ public class AtTests extends Common { Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance); + describeAt(repository, atAddress); + checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee); } } @@ -280,10 +396,10 @@ public class AtTests extends Common { BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT); BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT); - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient); + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); List executableAts = repository.getATRepository().getAllExecutableATs(); - + for (ATData atData : executableAts) { String atAddress = atData.getATAddress(); byte[] codeBytes = atData.getCodeBytes(); @@ -299,37 +415,13 @@ public class AtTests extends Common { if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH)) continue; - ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); - byte[] stateData = atStateData.getStateData(); - - QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); - byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); - - BTCACCT.AtConstants atConstants = BTCACCT.extractAtConstants(dataBytes); - - long autoRefundTimestamp = atData.getCreation() + atConstants.refundMinutes * 60 * 1000L; - - String autoRefundString = LocalDateTime.ofInstant(Instant.ofEpochMilli(autoRefundTimestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); - System.out.println(String.format("%s:\n" - + "\tcreator: %s,\n" - + "\tHASH160 of secret: %s,\n" - + "\trecipient: %s,\n" - + "\tinitial payout: %s QORT,\n" - + "\tredeem payout: %s QORT,\n" - + "\tauto-refund at: %s (local time)", - atAddress, - Crypto.toAddress(atData.getCreatorPublicKey()), - HashCode.fromBytes(atConstants.secretHash).toString().substring(0, 40), - atConstants.recipient, - atConstants.initialPayout.toPlainString(), - atConstants.redeemPayout.toPlainString(), - autoRefundString)); + describeAt(repository, atAddress); } } } - private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, Account recipient) throws DataException { - byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, recipient.getAddress(), refundTimeout, initialPayout, redeemAmount); + private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); long txTimestamp = System.currentTimeMillis(); byte[] lastReference = deployer.getLastReference(); @@ -341,7 +433,7 @@ public class AtTests extends Common { BigDecimal fee = BigDecimal.ZERO; String name = "QORT-BTC cross-chain trade"; - String description = String.format("Qortal-Bitcoin cross-chain trade between %s and %s", deployer.getAddress(), recipient.getAddress()); + String description = String.format("Qortal-Bitcoin cross-chain trade"); String atType = "ACCT"; String tags = "QORT-BTC ACCT"; @@ -358,7 +450,7 @@ public class AtTests extends Common { return deployAtTransaction; } - private MessageTransaction sendSecret(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException { + private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException { long txTimestamp = System.currentTimeMillis(); byte[] lastReference = sender.getLastReference(); @@ -400,4 +492,61 @@ public class AtTests extends Common { assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance.toPlainString(), expectedMaximumBalance.toPlainString()), actualBalance.compareTo(expectedMaximumBalance) < 0); } + private void describeAt(Repository repository, String atAddress) throws DataException { + ATData atData = repository.getATRepository().fromATAddress(atAddress); + + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + CrossChainTradeData tradeData = new CrossChainTradeData(); + tradeData.qortalAddress = atAddress; + tradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey()); + tradeData.creationTimestamp = atData.getCreation(); + tradeData.qortBalance = repository.getAccountRepository().getBalance(atAddress, Asset.QORT).getBalance(); + + BTCACCT.populateTradeData(tradeData, dataBytes); + + Function epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); + int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); + + System.out.print(String.format("%s:\n" + + "\tcreator: %s,\n" + + "\tcreation timestamp: %s,\n" + + "\tcurrent balance: %s QORT,\n" + + "\tHASH160 of secret: %s,\n" + + "\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, + tradeData.qortalCreator, + epochMilliFormatter.apply(tradeData.creationTimestamp), + tradeData.qortBalance.toPlainString(), + HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40), + 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)); + } else { + // Trade + System.out.println(String.format("\ttrade timeout: block %d,\n" + + "\ttrade recipient: %s", + tradeData.tradeRefundHeight, + tradeData.qortalRecipient)); + } + } + } diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java index 488de43f..87a64f7c 100644 --- a/src/test/java/org/qortal/test/btcacct/DeployAT.java +++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java @@ -2,16 +2,12 @@ package org.qortal.test.btcacct; import java.math.BigDecimal; import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; 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.BTCACCT; -import org.qortal.crypto.Crypto; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.TransactionData; @@ -38,14 +34,14 @@ public class DeployAT { if (error != null) System.err.println(error); - System.err.println(String.format("usage: DeployAT [ []]")); + System.err.println(String.format("usage: DeployAT [ []]")); System.err.println(String.format("example: DeployAT " + "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n" - + "\t3.1415 \\\n" - + "\tQgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v \\\n" + + "\t80.4020 \\\n" + + "\t0.00864200 \\\n" + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1585920000 \\\n" - + "\t0.0001")); + + "\t0.0001 \\\n" + + "\t123.456")); System.exit(1); } @@ -58,9 +54,8 @@ public class DeployAT { byte[] refundPrivateKey = null; BigDecimal redeemAmount = null; - String redeemAddress = null; + BigDecimal expectedBitcoin = null; byte[] secretHash = null; - int lockTime = 0; BigDecimal initialPayout = BigDecimal.ZERO.setScale(8); BigDecimal fundingAmount = null; @@ -74,16 +69,14 @@ public class DeployAT { if (redeemAmount.signum() <= 0) usage("QORT amount must be positive"); - redeemAddress = args[argIndex++]; - if (!Crypto.isValidAddress(redeemAddress)) - usage("Redeem address invalid"); + expectedBitcoin = new BigDecimal(args[argIndex++]).setScale(8); + if (expectedBitcoin.signum() <= 0) + usage("Expected BTC amount must be positive"); secretHash = HashCode.fromString(args[argIndex++]).asBytes(); if (secretHash.length != 20) usage("Hash of secret must be 20 bytes"); - lockTime = Integer.parseInt(args[argIndex++]); - if (args.length > argIndex) initialPayout = new BigDecimal(args[argIndex++]).setScale(8); @@ -118,20 +111,11 @@ public class DeployAT { System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); - System.out.println(String.format("Redeem Qortal address: %s", redeemAddress)); - - // New/derived info - - System.out.println("\nCHECKING info from other party:"); - - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime)); - System.out.println("Make sure this is BEFORE P2SH lockTime to allow you to refund AT before P2SH refunded"); - // Deploy AT - final int BLOCK_TIME = 60; // seconds - final int refundTimeout = (lockTime - (int) (System.currentTimeMillis() / 1000L)) / BLOCK_TIME; + final int offerTimeout = 2 * 60; // minutes + final int tradeTimeout = 60; // minutes - byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout, fundingAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, offerTimeout, tradeTimeout, initialPayout, fundingAmount, expectedBitcoin); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); long txTimestamp = System.currentTimeMillis(); @@ -144,7 +128,7 @@ public class DeployAT { BigDecimal fee = BigDecimal.ZERO; String name = "QORT-BTC cross-chain trade"; - String description = String.format("Qortal-Bitcoin cross-chain trade between %s and %s", refundAccount.getAddress(), redeemAddress); + String description = String.format("Qortal-Bitcoin cross-chain trade"); String atType = "ACCT"; String tags = "QORT-BTC ACCT";