mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-14 11:15:49 +00:00
Improved cross-chain AT and more API support for same.
Reworked the cross-chain trading AT so it is now 2-stage: stage 1: 'offer' mode waiting for message from creator containing trade partner's address stage 2: 'trade' mode waiting for message from trade partner containing secret Adjusted unit tests to cover above. Changed QortalATAPI.putCreatorAddressIntoB from storing creator's public key to actually storing creator's address. Refactored BTCACCT.AtConstants to CrossChainTradeData. Now we also store hash of AT's code bytes in DB so we can look up ATs by what they do. Affects ATData class, ATRepository, etc. Added "Automated Transactions" and "Cross-Chain" API sections. New API call GET /at/byfunction/{codehash} for looking up ATs by what they do, based on hash of their code bytes. New API call GET /at/{ataddress} for fetching info for specific AT. New API call GET /at/{ataddress}/data for fetch an AT's data segment. Mostly for diagnosis of AT's current state. New API call POST /at for building a raw, unsigned DEPLOY_AT transaction. New API call GET /crosschain/tradeoffers for finding open BTC-QORT trading ATs.
This commit is contained in:
parent
b93dca1818
commit
8baf42765e
@ -13,7 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
@Tag(name = "Admin"),
|
@Tag(name = "Admin"),
|
||||||
@Tag(name = "Arbitrary"),
|
@Tag(name = "Arbitrary"),
|
||||||
@Tag(name = "Assets"),
|
@Tag(name = "Assets"),
|
||||||
|
@Tag(name = "Automated Transactions"),
|
||||||
@Tag(name = "Blocks"),
|
@Tag(name = "Blocks"),
|
||||||
|
@Tag(name = "Cross-Chain"),
|
||||||
@Tag(name = "Groups"),
|
@Tag(name = "Groups"),
|
||||||
@Tag(name = "Names"),
|
@Tag(name = "Names"),
|
||||||
@Tag(name = "Payments"),
|
@Tag(name = "Payments"),
|
||||||
|
206
src/main/java/org/qortal/api/resource/AtResource.java
Normal file
206
src/main/java/org/qortal/api/resource/AtResource.java
Normal file
@ -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<ATData> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
105
src/main/java/org/qortal/api/resource/CrossChainResource.java
Normal file
105
src/main/java/org/qortal/api/resource/CrossChainResource.java
Normal file
@ -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<CrossChainTradeData> 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<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||||
|
|
||||||
|
List<CrossChainTradeData> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -58,7 +58,9 @@ public class AT {
|
|||||||
|
|
||||||
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
|
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.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
|
||||||
machineState.isFrozen(), machineState.getFrozenBalance());
|
machineState.isFrozen(), machineState.getFrozenBalance());
|
||||||
|
|
||||||
@ -104,9 +106,10 @@ public class AT {
|
|||||||
boolean hadFatalError = false;
|
boolean hadFatalError = false;
|
||||||
boolean isFrozen = false;
|
boolean isFrozen = false;
|
||||||
Long frozenBalance = null;
|
Long frozenBalance = null;
|
||||||
|
byte[] codeHash = Crypto.digest(codeBytes);
|
||||||
|
|
||||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, isSleeping, sleepUntilHeight, isFinished,
|
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, codeHash,
|
||||||
hadFatalError, isFrozen, frozenBalance);
|
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||||
|
|
||||||
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8), true);
|
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8), true);
|
||||||
}
|
}
|
||||||
|
@ -348,10 +348,15 @@ public class QortalATAPI extends API {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void putCreatorAddressIntoB(MachineState state) {
|
public void putCreatorAddressIntoB(MachineState state) {
|
||||||
// Simply use raw public key
|
|
||||||
byte[] publicKey = atData.getCreatorPublicKey();
|
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
|
@Override
|
||||||
|
@ -24,7 +24,9 @@ import org.ciyam.at.CompilationException;
|
|||||||
import org.ciyam.at.FunctionCode;
|
import org.ciyam.at.FunctionCode;
|
||||||
import org.ciyam.at.MachineState;
|
import org.ciyam.at.MachineState;
|
||||||
import org.ciyam.at.OpCode;
|
import org.ciyam.at.OpCode;
|
||||||
|
import org.ciyam.at.Timestamp;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.BitTwiddling;
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
@ -34,23 +36,7 @@ import com.google.common.primitives.Bytes;
|
|||||||
public class BTCACCT {
|
public class BTCACCT {
|
||||||
|
|
||||||
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
|
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 final byte[] CODE_BYTES_HASH = HashCode.fromString("da7271e9aa697112ece632cf2b462fded74843944a704b9d5fd4ae5971f6686f").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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* OP_TUCK (to copy public key to before signature)
|
* OP_TUCK (to copy public key to before signature)
|
||||||
@ -194,59 +180,98 @@ public class BTCACCT {
|
|||||||
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
|
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
|
// Labels for data segment addresses
|
||||||
int addrCounter = 0;
|
int addrCounter = 0;
|
||||||
|
|
||||||
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
||||||
final int addrHashPart1 = addrCounter++;
|
|
||||||
final int addrHashPart2 = addrCounter++;
|
final int addrQortalCreator1 = addrCounter++;
|
||||||
final int addrHashPart3 = addrCounter++;
|
final int addrQortalCreator2 = addrCounter++;
|
||||||
final int addrHashPart4 = addrCounter++;
|
final int addrQortalCreator3 = addrCounter++;
|
||||||
final int addrAddressPart1 = addrCounter++;
|
final int addrQortalCreator4 = addrCounter++;
|
||||||
final int addrAddressPart2 = addrCounter++;
|
|
||||||
final int addrAddressPart3 = addrCounter++;
|
final int addrSecretHash = addrCounter;
|
||||||
final int addrAddressPart4 = addrCounter++;
|
addrCounter += 4;
|
||||||
final int addrRefundMinutes = addrCounter++;
|
|
||||||
|
final int addrOfferTimeout = addrCounter++;
|
||||||
|
final int addrTradeTimeout = addrCounter++;
|
||||||
final int addrInitialPayoutAmount = addrCounter++;
|
final int addrInitialPayoutAmount = addrCounter++;
|
||||||
final int addrRedeemPayoutAmount = addrCounter++;
|
final int addrRedeemPayoutAmount = addrCounter++;
|
||||||
final int addrExpectedTxType = addrCounter++;
|
final int addrBitcoinAmount = addrCounter++;
|
||||||
final int addrHashIndex = addrCounter++;
|
|
||||||
final int addrAddressIndex = addrCounter++;
|
final int addrMessageTxType = addrCounter++;
|
||||||
final int addrAddressTempIndex = addrCounter++;
|
|
||||||
final int addrHashTempIndex = addrCounter++;
|
final int addrSecretHashPointer = addrCounter++;
|
||||||
final int addrHashTempLength = addrCounter++;
|
final int addrQortalRecipientPointer = addrCounter++;
|
||||||
|
final int addrMessageSenderPointer = addrCounter++;
|
||||||
|
|
||||||
|
final int addrMessageDataPointer = addrCounter++;
|
||||||
|
final int addrMessageDataLength = addrCounter++;
|
||||||
|
|
||||||
final int addrEndOfConstants = addrCounter;
|
final int addrEndOfConstants = addrCounter;
|
||||||
|
|
||||||
// Variables
|
// 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 addrBlockTimestamp = addrCounter++;
|
||||||
final int addrTxType = addrCounter++;
|
final int addrTxType = addrCounter++;
|
||||||
final int addrComparator = addrCounter++;
|
final int addrResult = addrCounter++;
|
||||||
final int addrAddressTemp1 = addrCounter++;
|
|
||||||
final int addrAddressTemp2 = addrCounter++;
|
final int addrMessageSender1 = addrCounter++;
|
||||||
final int addrAddressTemp3 = addrCounter++;
|
final int addrMessageSender2 = addrCounter++;
|
||||||
final int addrAddressTemp4 = addrCounter++;
|
final int addrMessageSender3 = addrCounter++;
|
||||||
final int addrHashTemp1 = addrCounter++;
|
final int addrMessageSender4 = addrCounter++;
|
||||||
final int addrHashTemp2 = addrCounter++;
|
|
||||||
final int addrHashTemp3 = addrCounter++;
|
final int addrMessageData = addrCounter;
|
||||||
final int addrHashTemp4 = addrCounter++;
|
addrCounter += 4;
|
||||||
|
|
||||||
// Data segment
|
// Data segment
|
||||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||||
|
|
||||||
// Hash of secret into HashPart1-4
|
// AT creator's Qortal address, decoded from Base58
|
||||||
assert dataByteBuffer.position() == addrHashPart1 * MachineState.VALUE_SIZE : "addrHashPart1 incorrect";
|
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));
|
dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0));
|
||||||
|
|
||||||
// Recipient Qortal address, decoded from Base58
|
// Open offer timeout in minutes
|
||||||
assert dataByteBuffer.position() == addrAddressPart1 * MachineState.VALUE_SIZE : "addrAddressPart1 incorrect";
|
assert dataByteBuffer.position() == addrOfferTimeout * MachineState.VALUE_SIZE : "addrOfferTimeout incorrect";
|
||||||
byte[] recipientAddressBytes = Base58.decode(recipientQortalAddress);
|
dataByteBuffer.putLong(offerTimeout);
|
||||||
dataByteBuffer.put(Bytes.ensureCapacity(recipientAddressBytes, 32, 0));
|
|
||||||
|
|
||||||
// Expiry in minutes
|
// Trade timeout in minutes
|
||||||
assert dataByteBuffer.position() == addrRefundMinutes * MachineState.VALUE_SIZE : "addrRefundMinutes incorrect";
|
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
||||||
dataByteBuffer.putLong(refundMinutes);
|
dataByteBuffer.putLong(tradeTimeout);
|
||||||
|
|
||||||
// Initial payout amount
|
// Initial payout amount
|
||||||
assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
|
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";
|
assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect";
|
||||||
dataByteBuffer.putLong(redeemPayout.unscaledValue().longValue());
|
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
|
// 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);
|
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
|
||||||
|
|
||||||
// Index into data segment of hash, used by GET_B_IND
|
// Index into data segment of hash, used by GET_B_IND
|
||||||
assert dataByteBuffer.position() == addrHashIndex * MachineState.VALUE_SIZE : "addrHashIndex incorrect";
|
assert dataByteBuffer.position() == addrSecretHashPointer * MachineState.VALUE_SIZE : "addrSecretHashPointer incorrect";
|
||||||
dataByteBuffer.putLong(addrHashPart1);
|
dataByteBuffer.putLong(addrSecretHash);
|
||||||
|
|
||||||
// Index into data segment of recipient address, used by SET_B_IND
|
// Index into data segment of recipient address, used by SET_B_IND
|
||||||
assert dataByteBuffer.position() == addrAddressIndex * MachineState.VALUE_SIZE : "addrAddressIndex incorrect";
|
assert dataByteBuffer.position() == addrQortalRecipientPointer * MachineState.VALUE_SIZE : "addrQortalRecipientPointer incorrect";
|
||||||
dataByteBuffer.putLong(addrAddressPart1);
|
dataByteBuffer.putLong(addrQortalRecipient1);
|
||||||
|
|
||||||
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
||||||
assert dataByteBuffer.position() == addrAddressTempIndex * MachineState.VALUE_SIZE : "addrAddressTempIndex incorrect";
|
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
|
||||||
dataByteBuffer.putLong(addrAddressTemp1);
|
dataByteBuffer.putLong(addrMessageSender1);
|
||||||
|
|
||||||
// Source location and length for hashing any passed secret
|
// Source location and length for hashing any passed secret
|
||||||
assert dataByteBuffer.position() == addrHashTempIndex * MachineState.VALUE_SIZE : "addrHashTempIndex incorrect";
|
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
|
||||||
dataByteBuffer.putLong(addrHashTemp1);
|
dataByteBuffer.putLong(addrMessageData);
|
||||||
assert dataByteBuffer.position() == addrHashTempLength * MachineState.VALUE_SIZE : "addrHashTempLength incorrect";
|
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
|
||||||
dataByteBuffer.putLong(32L);
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
|
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
|
||||||
|
|
||||||
// Code labels
|
// Code labels
|
||||||
Integer labelTxLoop = null;
|
|
||||||
Integer labelRefund = null;
|
Integer labelRefund = null;
|
||||||
Integer labelCheckTx = null;
|
|
||||||
|
Integer labelOfferTxLoop = null;
|
||||||
|
Integer labelCheckOfferTx = null;
|
||||||
|
|
||||||
|
Integer labelTradeTxLoop = null;
|
||||||
|
Integer labelCheckTradeTx = null;
|
||||||
|
|
||||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||||
|
|
||||||
@ -295,83 +328,136 @@ public class BTCACCT {
|
|||||||
/* Initialization */
|
/* Initialization */
|
||||||
|
|
||||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTimestamp));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||||
// 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));
|
|
||||||
|
|
||||||
// Load recipient's address into B register
|
// Calculate offer timeout refund 'timestamp' by adding addrOfferTimeout minutes to above 'timestamp', then save into addrOfferRefundTimestamp
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressIndex));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrOfferRefundTimestamp, addrLastTxTimestamp, addrOfferTimeout));
|
||||||
// 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));
|
|
||||||
|
|
||||||
// Set restart position to after this opcode
|
// Set restart position to after this opcode
|
||||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||||
|
|
||||||
/* Main loop */
|
/* Loop, waiting for offer timeout or message from AT owner containing trade partner details */
|
||||||
|
|
||||||
// Fetch current block 'timestamp'
|
// Fetch current block 'timestamp'
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
|
||||||
// If we're not past refund 'timestamp' then look for next transaction
|
// If we're not past offer timeout refund 'timestamp' then look for next transaction
|
||||||
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelTxLoop)));
|
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrOfferRefundTimestamp, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||||
// We're past refund 'timestamp' so go refund everything back to AT creator
|
// 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));
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
|
||||||
|
|
||||||
/* Transaction processing loop */
|
/* Transaction processing loop */
|
||||||
labelTxLoop = codeByteBuffer.position();
|
labelOfferTxLoop = codeByteBuffer.position();
|
||||||
|
|
||||||
// Find next transaction to this AT since the last one (if any)
|
// Find next transaction to this AT since the last one (if any)
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, 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.
|
// 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));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||||
// If addrComparator is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelCheckTx)));
|
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckOfferTx)));
|
||||||
// Stop and wait for next block
|
// Stop and wait for next block
|
||||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||||
|
|
||||||
/* Check transaction */
|
/* Check transaction */
|
||||||
labelCheckTx = codeByteBuffer.position();
|
labelCheckOfferTx = codeByteBuffer.position();
|
||||||
|
|
||||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, 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
|
// Extract transaction type (message/payment) from transaction and save type in addrTxType
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType));
|
||||||
// If transaction type is not MESSAGE type then go look for another transaction
|
// If transaction type is not MESSAGE type then go look for another transaction
|
||||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrExpectedTxType, calcOffset(codeByteBuffer, labelTxLoop)));
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||||
|
|
||||||
/* Check transaction's sender */
|
/* Check transaction's sender */
|
||||||
|
|
||||||
// Extract sender address from transaction into B register
|
// Extract sender address from transaction into B register
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
||||||
// Save B register into data segment starting at addrAddressTemp1 (as pointed to by 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, addrAddressTempIndex));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
|
||||||
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp1, addrAddressPart1, calcOffset(codeByteBuffer, labelTxLoop)));
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalCreator1, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp2, addrAddressPart2, calcOffset(codeByteBuffer, labelTxLoop)));
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalCreator2, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp3, addrAddressPart3, calcOffset(codeByteBuffer, labelTxLoop)));
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalCreator3, calcOffset(codeByteBuffer, labelOfferTxLoop)));
|
||||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp4, addrAddressPart4, calcOffset(codeByteBuffer, labelTxLoop)));
|
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 */
|
/* Check 'secret' in transaction's message */
|
||||||
|
|
||||||
// Extract message from transaction into B register
|
// Extract message from transaction into B register
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
||||||
// Save B register into data segment starting at addrHashTemp1 (as pointed to by addrHashTempIndex)
|
// 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, addrHashTempIndex));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
|
||||||
// Load B register with expected hash result (as pointed to by addrHashIndex)
|
// Load B register with expected hash result (as pointed to by addrSecretHashPointer)
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashIndex));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrSecretHashPointer));
|
||||||
// Perform HASH160 using source data at addrHashTemp1 through addrHashTemp4. (Location and length specified via addrHashTempIndex and addrHashTemplength).
|
// 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 addrComparator.
|
// 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, addrComparator, addrHashTempIndex, addrHashTempLength));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
|
||||||
// If hashes don't match, addrComparator will be zero so go find another transaction
|
// If hashes don't match, addrResult will be zero so go find another transaction
|
||||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelTxLoop)));
|
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelTradeTxLoop)));
|
||||||
|
|
||||||
/* Success! Pay arranged amount to intended recipient */
|
/* Success! Pay arranged amount to intended recipient */
|
||||||
|
|
||||||
// Load B register with intended recipient address (as pointed to by addrAddressIndex)
|
// Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer)
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressIndex));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer));
|
||||||
// Pay AT's balance to recipient
|
// Pay AT's balance to recipient
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount));
|
||||||
// We're finished forever
|
// Fall-through to refunding any remaining balance back to AT creator
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
|
||||||
|
|
||||||
/* Refund balance back to AT creator */
|
/* Refund balance back to AT creator */
|
||||||
labelRefund = codeByteBuffer.position();
|
labelRefund = codeByteBuffer.position();
|
||||||
@ -400,23 +486,63 @@ public class BTCACCT {
|
|||||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
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);
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes);
|
||||||
|
|
||||||
byte[] secretHash = new byte[32];
|
|
||||||
dataByteBuffer.get(secretHash);
|
|
||||||
|
|
||||||
byte[] addressBytes = new byte[32];
|
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);
|
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);
|
// Trade offer timeout (AT 'timestamp' converted to Qortal block height)
|
||||||
|
long tradeRefundTimestamp = dataByteBuffer.getLong();
|
||||||
BigDecimal redeemPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8);
|
if (tradeRefundTimestamp != 0)
|
||||||
|
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||||
return new AtConstants(secretHash, initialPayout, redeemPayout, recipient, refundMinutes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,11 @@ package org.qortal.data.at;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
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 {
|
public class ATData {
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
@ -11,6 +16,7 @@ public class ATData {
|
|||||||
private int version;
|
private int version;
|
||||||
private long assetId;
|
private long assetId;
|
||||||
private byte[] codeBytes;
|
private byte[] codeBytes;
|
||||||
|
private byte[] codeHash;
|
||||||
private boolean isSleeping;
|
private boolean isSleeping;
|
||||||
private Integer sleepUntilHeight;
|
private Integer sleepUntilHeight;
|
||||||
private boolean isFinished;
|
private boolean isFinished;
|
||||||
@ -20,14 +26,19 @@ public class ATData {
|
|||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
|
// necessary for JAX-RS serialization
|
||||||
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) {
|
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.ATAddress = ATAddress;
|
||||||
this.creatorPublicKey = creatorPublicKey;
|
this.creatorPublicKey = creatorPublicKey;
|
||||||
this.creation = creation;
|
this.creation = creation;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.assetId = assetId;
|
this.assetId = assetId;
|
||||||
this.codeBytes = codeBytes;
|
this.codeBytes = codeBytes;
|
||||||
|
this.codeHash = codeHash;
|
||||||
this.isSleeping = isSleeping;
|
this.isSleeping = isSleeping;
|
||||||
this.sleepUntilHeight = sleepUntilHeight;
|
this.sleepUntilHeight = sleepUntilHeight;
|
||||||
this.isFinished = isFinished;
|
this.isFinished = isFinished;
|
||||||
@ -36,10 +47,10 @@ public class ATData {
|
|||||||
this.frozenBalance = frozenBalance;
|
this.frozenBalance = frozenBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
|
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash,
|
||||||
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
|
boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
|
||||||
this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
|
this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash,
|
||||||
(BigDecimal) null);
|
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null);
|
||||||
|
|
||||||
// Convert Long frozenBalance to BigDecimal
|
// Convert Long frozenBalance to BigDecimal
|
||||||
if (frozenBalance != null)
|
if (frozenBalance != null)
|
||||||
@ -80,6 +91,10 @@ public class ATData {
|
|||||||
return this.codeBytes;
|
return this.codeBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getCodeHash() {
|
||||||
|
return this.codeHash;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getIsSleeping() {
|
public boolean getIsSleeping() {
|
||||||
return this.isSleeping;
|
return this.isSleeping;
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,9 @@ public interface ATRepository {
|
|||||||
/** Returns list of executable ATs, empty if none found */
|
/** Returns list of executable ATs, empty if none found */
|
||||||
public List<ATData> getAllExecutableATs() throws DataException;
|
public List<ATData> getAllExecutableATs() throws DataException;
|
||||||
|
|
||||||
|
/** Returns list of ATs with matching code hash, optionally executable only. */
|
||||||
|
public List<ATData> 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 */
|
/** Returns creation block height given AT's address or null if not found */
|
||||||
public Integer getATCreationBlockHeight(String atAddress) throws DataException;
|
public Integer getATCreationBlockHeight(String atAddress) throws DataException;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ATData fromATAddress(String atAddress) throws DataException {
|
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_sleeping, sleep_until_height, is_finished, had_fatal_error, "
|
||||||
+ "is_frozen, frozen_balance "
|
+ "is_frozen, frozen_balance "
|
||||||
+ "FROM ATs "
|
+ "FROM ATs "
|
||||||
@ -41,20 +41,21 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
int version = resultSet.getInt(3);
|
int version = resultSet.getInt(3);
|
||||||
long assetId = resultSet.getLong(4);
|
long assetId = resultSet.getLong(4);
|
||||||
byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB
|
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())
|
if (sleepUntilHeight == 0 && resultSet.wasNull())
|
||||||
sleepUntilHeight = null;
|
sleepUntilHeight = null;
|
||||||
|
|
||||||
boolean isFinished = resultSet.getBoolean(8);
|
boolean isFinished = resultSet.getBoolean(9);
|
||||||
boolean hadFatalError = resultSet.getBoolean(9);
|
boolean hadFatalError = resultSet.getBoolean(10);
|
||||||
boolean isFrozen = 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,
|
return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash,
|
||||||
isFrozen, frozenBalance);
|
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch AT from repository", e);
|
throw new DataException("Unable to fetch AT from repository", e);
|
||||||
}
|
}
|
||||||
@ -71,7 +72,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ATData> getAllExecutableATs() throws DataException {
|
public List<ATData> 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_sleeping, sleep_until_height, had_fatal_error, "
|
||||||
+ "is_frozen, frozen_balance "
|
+ "is_frozen, frozen_balance "
|
||||||
+ "FROM ATs "
|
+ "FROM ATs "
|
||||||
@ -93,19 +94,20 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
int version = resultSet.getInt(4);
|
int version = resultSet.getInt(4);
|
||||||
long assetId = resultSet.getLong(5);
|
long assetId = resultSet.getLong(5);
|
||||||
byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB
|
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())
|
if (sleepUntilHeight == 0 && resultSet.wasNull())
|
||||||
sleepUntilHeight = null;
|
sleepUntilHeight = null;
|
||||||
|
|
||||||
boolean hadFatalError = resultSet.getBoolean(9);
|
boolean hadFatalError = resultSet.getBoolean(10);
|
||||||
boolean isFrozen = 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,
|
ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash,
|
||||||
hadFatalError, isFrozen, frozenBalance);
|
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||||
|
|
||||||
executableATs.add(atData);
|
executableATs.add(atData);
|
||||||
} while (resultSet.next());
|
} while (resultSet.next());
|
||||||
@ -116,6 +118,62 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ATData> 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<ATData> 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
|
@Override
|
||||||
public Integer getATCreationBlockHeight(String atAddress) throws DataException {
|
public Integer getATCreationBlockHeight(String atAddress) throws DataException {
|
||||||
String sql = "SELECT height "
|
String sql = "SELECT height "
|
||||||
@ -140,7 +198,8 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
|
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
|
||||||
|
|
||||||
saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(atData.getCreation()))
|
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_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("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
|
||||||
.bind("frozen_balance", atData.getFrozenBalance());
|
.bind("frozen_balance", atData.getFrozenBalance());
|
||||||
|
@ -980,6 +980,11 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE");
|
stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE");
|
||||||
break;
|
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:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
@ -10,7 +10,9 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.time.format.FormatStyle;
|
import java.time.format.FormatStyle;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Base58;
|
||||||
import org.ciyam.at.MachineState;
|
import org.ciyam.at.MachineState;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -22,6 +24,7 @@ import org.qortal.crosschain.BTCACCT;
|
|||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.at.ATStateData;
|
import org.qortal.data.at.ATStateData;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
import org.qortal.data.transaction.MessageTransactionData;
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
@ -37,6 +40,7 @@ import org.qortal.transaction.DeployAtTransaction;
|
|||||||
import org.qortal.transaction.MessageTransaction;
|
import org.qortal.transaction.MessageTransaction;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
|
||||||
public class AtTests extends Common {
|
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 initialPayout = new BigDecimal("0.001").setScale(8);
|
||||||
public static final BigDecimal redeemAmount = new BigDecimal("80.4020").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 fundingAmount = new BigDecimal("123.456").setScale(8);
|
||||||
|
public static final BigDecimal bitcoinAmount = new BigDecimal("0.00864200").setScale(8);
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void beforeTest() throws DataException {
|
public void beforeTest() throws DataException {
|
||||||
@ -54,9 +59,9 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCompile() {
|
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());
|
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 deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.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 expectedBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtTransaction.getTransactionData().getFee());
|
||||||
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
|
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")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@Test
|
||||||
public void testInitialPayment() throws DataException {
|
public void testInitialPayment() throws DataException {
|
||||||
@ -116,9 +152,15 @@ public class AtTests extends Common {
|
|||||||
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.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);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
BigDecimal expectedBalance = recipientsInitialBalance.add(initialPayout);
|
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);
|
Common.assertEqualBigDecimals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
// Test orphaning
|
// Test orphaning
|
||||||
BlockUtils.orphanLastBlock(repository);
|
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")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@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()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||||
@ -146,15 +222,28 @@ public class AtTests extends Common {
|
|||||||
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.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 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);
|
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
// Test orphaning
|
// Test orphaning
|
||||||
BlockUtils.orphanLastBlock(repository);
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
BigDecimal expectedBalance = deployersPostDeploymentBalance;
|
BigDecimal expectedBalance = deployersPostDeploymentBalance;
|
||||||
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
|
||||||
@ -173,12 +262,19 @@ public class AtTests extends Common {
|
|||||||
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
|
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
|
||||||
|
|
||||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
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
|
// 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
|
// AT should send funds in the next block
|
||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
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);
|
Common.assertEqualBigDecimals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
// Orphan redeem
|
// Orphan redeem
|
||||||
BlockUtils.orphanLastBlock(repository);
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
@ -215,14 +313,21 @@ public class AtTests extends Common {
|
|||||||
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.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();
|
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||||
|
|
||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
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
|
// 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
|
// AT should NOT send funds in the next block
|
||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
@ -233,6 +338,8 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,15 +354,22 @@ public class AtTests extends Common {
|
|||||||
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.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();
|
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||||
|
|
||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
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
|
// Send correct secret to AT, but from wrong account
|
||||||
byte[] wrongSecret = Crypto.digest(secret);
|
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
|
// AT should NOT send funds in the next block
|
||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
@ -266,6 +380,8 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,10 +396,10 @@ public class AtTests extends Common {
|
|||||||
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
|
||||||
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
|
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
|
||||||
|
|
||||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||||
|
|
||||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||||
|
|
||||||
for (ATData atData : executableAts) {
|
for (ATData atData : executableAts) {
|
||||||
String atAddress = atData.getATAddress();
|
String atAddress = atData.getATAddress();
|
||||||
byte[] codeBytes = atData.getCodeBytes();
|
byte[] codeBytes = atData.getCodeBytes();
|
||||||
@ -299,37 +415,13 @@ public class AtTests extends Common {
|
|||||||
if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH))
|
if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
describeAt(repository, 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, Account recipient) throws DataException {
|
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, recipient.getAddress(), refundTimeout, initialPayout, redeemAmount);
|
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||||
|
|
||||||
long txTimestamp = System.currentTimeMillis();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
byte[] lastReference = deployer.getLastReference();
|
byte[] lastReference = deployer.getLastReference();
|
||||||
@ -341,7 +433,7 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
BigDecimal fee = BigDecimal.ZERO;
|
BigDecimal fee = BigDecimal.ZERO;
|
||||||
String name = "QORT-BTC cross-chain trade";
|
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 atType = "ACCT";
|
||||||
String tags = "QORT-BTC ACCT";
|
String tags = "QORT-BTC ACCT";
|
||||||
|
|
||||||
@ -358,7 +450,7 @@ public class AtTests extends Common {
|
|||||||
return deployAtTransaction;
|
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();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
byte[] lastReference = sender.getLastReference();
|
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);
|
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<Long, String> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,12 @@ package org.qortal.test.btcacct;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
import org.qortal.crypto.Crypto;
|
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
@ -38,14 +34,14 @@ public class DeployAT {
|
|||||||
if (error != null)
|
if (error != null)
|
||||||
System.err.println(error);
|
System.err.println(error);
|
||||||
|
|
||||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <redeem Qortal address> <HASH160-of-secret> <locktime> [<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>]]"));
|
||||||
System.err.println(String.format("example: DeployAT "
|
System.err.println(String.format("example: DeployAT "
|
||||||
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
||||||
+ "\t3.1415 \\\n"
|
+ "\t80.4020 \\\n"
|
||||||
+ "\tQgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v \\\n"
|
+ "\t0.00864200 \\\n"
|
||||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||||
+ "\t1585920000 \\\n"
|
+ "\t0.0001 \\\n"
|
||||||
+ "\t0.0001"));
|
+ "\t123.456"));
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,9 +54,8 @@ public class DeployAT {
|
|||||||
|
|
||||||
byte[] refundPrivateKey = null;
|
byte[] refundPrivateKey = null;
|
||||||
BigDecimal redeemAmount = null;
|
BigDecimal redeemAmount = null;
|
||||||
String redeemAddress = null;
|
BigDecimal expectedBitcoin = null;
|
||||||
byte[] secretHash = null;
|
byte[] secretHash = null;
|
||||||
int lockTime = 0;
|
|
||||||
BigDecimal initialPayout = BigDecimal.ZERO.setScale(8);
|
BigDecimal initialPayout = BigDecimal.ZERO.setScale(8);
|
||||||
BigDecimal fundingAmount = null;
|
BigDecimal fundingAmount = null;
|
||||||
|
|
||||||
@ -74,16 +69,14 @@ public class DeployAT {
|
|||||||
if (redeemAmount.signum() <= 0)
|
if (redeemAmount.signum() <= 0)
|
||||||
usage("QORT amount must be positive");
|
usage("QORT amount must be positive");
|
||||||
|
|
||||||
redeemAddress = args[argIndex++];
|
expectedBitcoin = new BigDecimal(args[argIndex++]).setScale(8);
|
||||||
if (!Crypto.isValidAddress(redeemAddress))
|
if (expectedBitcoin.signum() <= 0)
|
||||||
usage("Redeem address invalid");
|
usage("Expected BTC amount must be positive");
|
||||||
|
|
||||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
if (secretHash.length != 20)
|
if (secretHash.length != 20)
|
||||||
usage("Hash of secret must be 20 bytes");
|
usage("Hash of secret must be 20 bytes");
|
||||||
|
|
||||||
lockTime = Integer.parseInt(args[argIndex++]);
|
|
||||||
|
|
||||||
if (args.length > argIndex)
|
if (args.length > argIndex)
|
||||||
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
|
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("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
|
// Deploy AT
|
||||||
final int BLOCK_TIME = 60; // seconds
|
final int offerTimeout = 2 * 60; // minutes
|
||||||
final int refundTimeout = (lockTime - (int) (System.currentTimeMillis() / 1000L)) / BLOCK_TIME;
|
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());
|
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||||
|
|
||||||
long txTimestamp = System.currentTimeMillis();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
@ -144,7 +128,7 @@ public class DeployAT {
|
|||||||
|
|
||||||
BigDecimal fee = BigDecimal.ZERO;
|
BigDecimal fee = BigDecimal.ZERO;
|
||||||
String name = "QORT-BTC cross-chain trade";
|
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 atType = "ACCT";
|
||||||
String tags = "QORT-BTC ACCT";
|
String tags = "QORT-BTC ACCT";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user