forked from Qortal/qortal
WIP: cross-chain trading AT passes AtTests now
This commit is contained in:
parent
23062c59cd
commit
886c9156a5
@ -13,6 +13,8 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.ToIntFunction;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
@ -392,9 +394,9 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/p2sh")
|
@Path("/p2sh/a")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns Bitcoin P2SH address based on trade info",
|
summary = "Returns Bitcoin P2SH-A address based on trade info",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -411,7 +413,35 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
public String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest) {
|
public String deriveP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||||
|
return deriveP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/p2sh/b")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns Bitcoin P2SH-B address based on trade info",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainBitcoinTemplateRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String deriveP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||||
|
return deriveP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
|
||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
NetworkParameters params = btc.getNetworkParameters();
|
NetworkParameters params = btc.getNetworkParameters();
|
||||||
|
|
||||||
@ -432,7 +462,7 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.hashOfSecretB);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, lockTimeFn.applyAsInt(crossChainTradeData), templateRequest.redeemPublicKeyHash, hashOfSecretFn.apply(crossChainTradeData));
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -443,9 +473,9 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/p2sh/check")
|
@Path("/p2sh/a/check")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks Bitcoin P2SH address based on trade info",
|
summary = "Checks Bitcoin P2SH-A address based on trade info",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -462,7 +492,35 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||||
public CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest) {
|
public CrossChainBitcoinP2SHStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||||
|
return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/p2sh/b/check")
|
||||||
|
@Operation(
|
||||||
|
summary = "Checks Bitcoin P2SH-B address based on trade info",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainBitcoinTemplateRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinP2SHStatus.class))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public CrossChainBitcoinP2SHStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
|
||||||
|
return checkP2sh(templateRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
|
||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
NetworkParameters params = btc.getNetworkParameters();
|
NetworkParameters params = btc.getNetworkParameters();
|
||||||
|
|
||||||
@ -483,7 +541,10 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.hashOfSecretB);
|
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
|
||||||
|
byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData);
|
||||||
|
|
||||||
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, lockTime, templateRequest.redeemPublicKeyHash, hashOfSecret);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -508,7 +569,7 @@ public class CrossChainResource {
|
|||||||
|
|
||||||
if (p2shBalance >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) {
|
if (p2shBalance >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) {
|
||||||
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
|
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
|
||||||
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
p2shStatus.canRefund = now >= lockTime * 1000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (now >= medianBlockTime * 1000L) {
|
if (now >= medianBlockTime * 1000L) {
|
||||||
@ -524,9 +585,9 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/p2sh/refund")
|
@Path("/p2sh/a/refund")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns serialized Bitcoin transaction attempting refund from P2SH address",
|
summary = "Returns serialized Bitcoin transaction attempting refund from P2SH-A address",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -544,7 +605,36 @@ public class CrossChainResource {
|
|||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||||
public String refundP2sh(CrossChainBitcoinRefundRequest refundRequest) {
|
public String refundP2shA(CrossChainBitcoinRefundRequest refundRequest) {
|
||||||
|
return refundP2sh(refundRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/p2sh/b/refund")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns serialized Bitcoin transaction attempting refund from P2SH-B address",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainBitcoinRefundRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||||
|
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String refundP2shB(CrossChainBitcoinRefundRequest refundRequest) {
|
||||||
|
return refundP2sh(refundRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String refundP2sh(CrossChainBitcoinRefundRequest refundRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
|
||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
NetworkParameters params = btc.getNetworkParameters();
|
NetworkParameters params = btc.getNetworkParameters();
|
||||||
|
|
||||||
@ -580,7 +670,10 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.hashOfSecretB);
|
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
|
||||||
|
byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData);
|
||||||
|
|
||||||
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), lockTime, refundRequest.redeemPublicKeyHash, hashOfSecret);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -597,7 +690,7 @@ public class CrossChainResource {
|
|||||||
if (fundingOutputs.isEmpty())
|
if (fundingOutputs.isEmpty())
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
boolean canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
boolean canRefund = now >= lockTime * 1000L;
|
||||||
if (!canRefund)
|
if (!canRefund)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
|
||||||
|
|
||||||
@ -606,7 +699,7 @@ public class CrossChainResource {
|
|||||||
|
|
||||||
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
||||||
|
|
||||||
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime);
|
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
||||||
|
|
||||||
if (!wasBroadcast)
|
if (!wasBroadcast)
|
||||||
@ -619,9 +712,9 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/p2sh/redeem")
|
@Path("/p2sh/a/redeem")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns serialized Bitcoin transaction attempting redeem from P2SH address",
|
summary = "Returns serialized Bitcoin transaction attempting redeem from P2SH-A address",
|
||||||
requestBody = @RequestBody(
|
requestBody = @RequestBody(
|
||||||
required = true,
|
required = true,
|
||||||
content = @Content(
|
content = @Content(
|
||||||
@ -639,7 +732,36 @@ public class CrossChainResource {
|
|||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||||
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||||
public String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest) {
|
public String redeemP2shA(CrossChainBitcoinRedeemRequest redeemRequest) {
|
||||||
|
return redeemP2sh(redeemRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeA, (crossChainTradeData) -> crossChainTradeData.hashOfSecretA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/p2sh/b/redeem")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns serialized Bitcoin transaction attempting redeem from P2SH-B address",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainBitcoinRedeemRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
|
||||||
|
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String redeemP2shB(CrossChainBitcoinRedeemRequest redeemRequest) {
|
||||||
|
return redeemP2sh(redeemRequest, (crossChainTradeData) -> crossChainTradeData.lockTimeB, (crossChainTradeData) -> crossChainTradeData.hashOfSecretB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest, ToIntFunction<CrossChainTradeData> lockTimeFn, Function<CrossChainTradeData, byte[]> hashOfSecretFn) {
|
||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
NetworkParameters params = btc.getNetworkParameters();
|
NetworkParameters params = btc.getNetworkParameters();
|
||||||
|
|
||||||
@ -678,7 +800,10 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.hashOfSecretB);
|
int lockTime = lockTimeFn.applyAsInt(crossChainTradeData);
|
||||||
|
byte[] hashOfSecret = hashOfSecretFn.apply(crossChainTradeData);
|
||||||
|
|
||||||
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, lockTime, redeemKey.getPubKeyHash(), hashOfSecret);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
@ -299,15 +299,12 @@ public class QortalATAPI extends API {
|
|||||||
|
|
||||||
byte[] messageData = this.getMessageFromTransaction(transactionData);
|
byte[] messageData = this.getMessageFromTransaction(transactionData);
|
||||||
|
|
||||||
// Check data length is appropriate, i.e. not larger than B
|
|
||||||
if (messageData.length > 4 * 8)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Pad messageData to fit B
|
// Pad messageData to fit B
|
||||||
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
|
if (messageData.length < 4 * 8)
|
||||||
|
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
|
||||||
|
|
||||||
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
|
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
|
||||||
this.setB(state, paddedMessageData);
|
this.setB(state, messageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -34,7 +34,6 @@ import org.qortal.transaction.MessageTransaction;
|
|||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||||
import org.qortal.utils.Base58;
|
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class TradeBot {
|
public class TradeBot {
|
||||||
@ -123,7 +122,7 @@ public class TradeBot {
|
|||||||
repository.getCrossChainRepository().save(tradeBotData);
|
repository.getCrossChainRepository().save(tradeBotData);
|
||||||
|
|
||||||
// P2SH_a to be funded
|
// P2SH_a to be funded
|
||||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.creatorBitcoinPKH, secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, secretHash);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -189,7 +188,7 @@ public class TradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long tradeStartTimestamp = atData.getCreation();
|
long atCreationTimestamp = atData.getCreation();
|
||||||
|
|
||||||
String address = Crypto.toAddress(tradeBotData.getTradeNativePublicKey());
|
String address = Crypto.toAddress(tradeBotData.getTradeNativePublicKey());
|
||||||
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(address, null, null, null);
|
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(address, null, null, null);
|
||||||
@ -223,10 +222,10 @@ public class TradeBot {
|
|||||||
byte[] aliceForeignPublicKeyHash = new byte[20];
|
byte[] aliceForeignPublicKeyHash = new byte[20];
|
||||||
System.arraycopy(messageData, 20, aliceForeignPublicKeyHash, 0, 20);
|
System.arraycopy(messageData, 20, aliceForeignPublicKeyHash, 0, 20);
|
||||||
|
|
||||||
// Determine P2SH address and confirm funded
|
// Determine P2SH-A address and confirm funded
|
||||||
// First P2SH refund timeout is last in chain, so add all of tradeTimeout
|
// First P2SH-A refund timeout is last in chain, so add all of tradeTimeout
|
||||||
int lockTime = (int) (tradeStartTimestamp / 1000L + tradeBotData.getTradeTimeout() * 60);
|
int lockTimeA = BTCACCT.calcLockTimeA(atCreationTimestamp, tradeBotData.getTradeTimeout());
|
||||||
byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
|
byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
|
||||||
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
|
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
|
||||||
|
|
||||||
Long balance = BTC.getInstance().getBalance(p2shAddress);
|
Long balance = BTC.getInstance().getBalance(p2shAddress);
|
||||||
@ -235,13 +234,10 @@ public class TradeBot {
|
|||||||
|
|
||||||
// Good to go - send MESSAGE to AT
|
// Good to go - send MESSAGE to AT
|
||||||
|
|
||||||
byte[] aliceNativeAddress = Base58.decode(Crypto.toAddress(messageTransactionData.getCreatorPublicKey()));
|
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
||||||
|
|
||||||
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||||
byte[] outgoingMessageData = new byte[96];
|
byte[] outgoingMessageData = BTCACCT.buildOfferMessage(aliceNativeAddress, aliceForeignPublicKeyHash, aliceSecretHash);
|
||||||
System.arraycopy(aliceNativeAddress, 0, outgoingMessageData, 0, aliceNativeAddress.length);
|
|
||||||
System.arraycopy(aliceForeignPublicKeyHash, 0, outgoingMessageData, 32, 20);
|
|
||||||
System.arraycopy(aliceSecretHash, 0, outgoingMessageData, 64, 20);
|
|
||||||
|
|
||||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), outgoingMessageData, false, false);
|
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), outgoingMessageData, false, false);
|
||||||
|
@ -107,6 +107,8 @@ public class BTCACCT {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, int tradeTimeout, long qortAmount, long bitcoinAmount) {
|
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, int tradeTimeout, long qortAmount, long bitcoinAmount) {
|
||||||
|
int refundTimeout = calcRefundTimeout(tradeTimeout);
|
||||||
|
|
||||||
// Labels for data segment addresses
|
// Labels for data segment addresses
|
||||||
int addrCounter = 0;
|
int addrCounter = 0;
|
||||||
|
|
||||||
@ -207,7 +209,7 @@ public class BTCACCT {
|
|||||||
|
|
||||||
// Refund timeout in minutes (¾ of trade-timeout)
|
// Refund timeout in minutes (¾ of trade-timeout)
|
||||||
assert dataByteBuffer.position() == addrRefundTimeout * MachineState.VALUE_SIZE : "addrRefundTimeout incorrect";
|
assert dataByteBuffer.position() == addrRefundTimeout * MachineState.VALUE_SIZE : "addrRefundTimeout incorrect";
|
||||||
dataByteBuffer.putLong(tradeTimeout * 3 / 4);
|
dataByteBuffer.putLong(refundTimeout);
|
||||||
|
|
||||||
// Redeem Qort amount
|
// Redeem Qort amount
|
||||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||||
@ -263,7 +265,7 @@ public class BTCACCT {
|
|||||||
|
|
||||||
// Offset into TRADE MESSAGE data payload for extracting secret-B
|
// Offset into TRADE MESSAGE data payload for extracting secret-B
|
||||||
assert dataByteBuffer.position() == addrTradeMessageSecretBOffset * MachineState.VALUE_SIZE : "addrTradeMessageSecretBOffset incorrect";
|
assert dataByteBuffer.position() == addrTradeMessageSecretBOffset * MachineState.VALUE_SIZE : "addrTradeMessageSecretBOffset incorrect";
|
||||||
dataByteBuffer.putLong(64L);
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
// Source location and length for hashing any passed secret
|
// Source location and length for hashing any passed secret
|
||||||
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
|
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
|
||||||
@ -634,9 +636,10 @@ public class BTCACCT {
|
|||||||
// Skip temporary message data
|
// Skip temporary message data
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||||
|
|
||||||
// Potential hash of secret A
|
// Potential hash160 of secret A
|
||||||
byte[] hashOfSecretA = new byte[32];
|
byte[] hashOfSecretA = new byte[20];
|
||||||
dataByteBuffer.get(hashOfSecretA);
|
dataByteBuffer.get(hashOfSecretA);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
||||||
|
|
||||||
// Potential recipient's Bitcoin PKH
|
// Potential recipient's Bitcoin PKH
|
||||||
byte[] recipientBitcoinPKH = new byte[20];
|
byte[] recipientBitcoinPKH = new byte[20];
|
||||||
@ -651,6 +654,8 @@ public class BTCACCT {
|
|||||||
tradeData.qortalRecipient = qortalRecipient;
|
tradeData.qortalRecipient = qortalRecipient;
|
||||||
tradeData.hashOfSecretA = hashOfSecretA;
|
tradeData.hashOfSecretA = hashOfSecretA;
|
||||||
tradeData.recipientBitcoinPKH = recipientBitcoinPKH;
|
tradeData.recipientBitcoinPKH = recipientBitcoinPKH;
|
||||||
|
tradeData.lockTimeA = calcLockTimeA(tradeData.creationTimestamp, tradeData.tradeTimeout);
|
||||||
|
tradeData.lockTimeB = calcLockTimeB(tradeData.creationTimestamp, tradeData.tradeTimeout);
|
||||||
} else {
|
} else {
|
||||||
tradeData.mode = CrossChainTradeData.Mode.OFFER;
|
tradeData.mode = CrossChainTradeData.Mode.OFFER;
|
||||||
}
|
}
|
||||||
@ -658,4 +663,51 @@ public class BTCACCT {
|
|||||||
return tradeData;
|
return tradeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns trade-info MESSAGE payload for AT creator to send to AT. */
|
||||||
|
public static byte[] buildOfferMessage(String recipientQortalAddress, byte[] recipientBitcoinPKH, byte[] hashOfSecretA) {
|
||||||
|
byte[] data = new byte[32 + 32 + 32];
|
||||||
|
byte[] recipientQortalAddressBytes = Base58.decode(recipientQortalAddress);
|
||||||
|
|
||||||
|
System.arraycopy(recipientQortalAddressBytes, 0, data, 0, recipientQortalAddressBytes.length);
|
||||||
|
System.arraycopy(recipientBitcoinPKH, 0, data, 32, recipientBitcoinPKH.length);
|
||||||
|
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns refund MESSAGE payload for AT creator to cancel trade AT. */
|
||||||
|
public static byte[] buildRefundMessage(String creatorQortalAddress) {
|
||||||
|
byte[] data = new byte[32];
|
||||||
|
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
||||||
|
|
||||||
|
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns redeem MESSAGE payload for trade partner/recipient to send to AT. */
|
||||||
|
public static byte[] buildTradeMessage(byte[] secretA, byte[] secretB) {
|
||||||
|
byte[] data = new byte[32 + 32];
|
||||||
|
|
||||||
|
System.arraycopy(secretA, 0, data, 0, secretA.length);
|
||||||
|
System.arraycopy(secretB, 0, data, 32, secretB.length);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns AT refundTimeout (minutes) based on tradeTimeout. */
|
||||||
|
public static int calcRefundTimeout(int tradeTimeout) {
|
||||||
|
return tradeTimeout * 3 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns P2SH-A lockTime (epoch seconds). */
|
||||||
|
public static int calcLockTimeA(long atCreationTimestamp, int tradeTimeout) {
|
||||||
|
return (int) (atCreationTimestamp / 1000L + tradeTimeout * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns P2SH-B lockTime (epoch seconds). */
|
||||||
|
public static int calcLockTimeB(long atCreationTimestamp, int tradeTimeout) {
|
||||||
|
return (int) (atCreationTimestamp / 1000L + tradeTimeout / 2 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -64,8 +64,11 @@ public class CrossChainTradeData {
|
|||||||
|
|
||||||
public Mode mode;
|
public Mode mode;
|
||||||
|
|
||||||
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout")
|
@Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
|
||||||
public Integer lockTime;
|
public Integer lockTimeA;
|
||||||
|
|
||||||
|
@Schema(description = "Suggested Bitcoin P2SH-B nLockTime based on trade timeout")
|
||||||
|
public Integer lockTimeB;
|
||||||
|
|
||||||
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
|
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
|
||||||
public byte[] recipientBitcoinPKH;
|
public byte[] recipientBitcoinPKH;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.qortal.test.btcacct;
|
package org.qortal.test.btcacct;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.*;
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -10,6 +9,7 @@ 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.Random;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.bitcoinj.core.Base58;
|
import org.bitcoinj.core.Base58;
|
||||||
@ -18,6 +18,7 @@ import org.junit.Test;
|
|||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.block.Block;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
@ -43,10 +44,12 @@ import com.google.common.primitives.Bytes;
|
|||||||
|
|
||||||
public class AtTests extends Common {
|
public class AtTests extends Common {
|
||||||
|
|
||||||
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes();
|
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||||
public static final byte[] bitcoinPublicKeyHash = new byte[20]; // not used in tests
|
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||||
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
|
||||||
public static final int refundTimeout = 10; // blocks
|
public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
|
||||||
|
public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
|
||||||
|
public static final int tradeTimeout = 12; // blocks
|
||||||
public static final long redeemAmount = 80_40200000L;
|
public static final long redeemAmount = 80_40200000L;
|
||||||
public static final long fundingAmount = 123_45600000L;
|
public static final long fundingAmount = 123_45600000L;
|
||||||
public static final long bitcoinAmount = 864200L;
|
public static final long bitcoinAmount = 864200L;
|
||||||
@ -60,7 +63,7 @@ public class AtTests extends Common {
|
|||||||
public void testCompile() {
|
public void testCompile() {
|
||||||
Account deployer = Common.getTestAccount(null, "chloe");
|
Account deployer = Common.getTestAccount(null, "chloe");
|
||||||
|
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount);
|
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, tradeTimeout, redeemAmount, bitcoinAmount);
|
||||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +148,10 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
describeAt(repository, atAddress);
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
// Check AT is finished
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
assertTrue(atData.getIsFinished());
|
||||||
|
|
||||||
// Test orphaning
|
// Test orphaning
|
||||||
BlockUtils.orphanLastBlock(repository);
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
@ -155,7 +162,59 @@ public class AtTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
@SuppressWarnings("unused")
|
||||||
|
@Test
|
||||||
|
public void testTradingInfoProcessing() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||||
|
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||||
|
|
||||||
|
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||||
|
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||||
|
Account at = deployAtTransaction.getATAccount();
|
||||||
|
String atAddress = at.getAddress();
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA);
|
||||||
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
|
|
||||||
|
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||||
|
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||||
|
|
||||||
|
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||||
|
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||||
|
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee;
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||||
|
|
||||||
|
// AT should be in TRADE mode
|
||||||
|
assertEquals(CrossChainTradeData.Mode.TRADE, tradeData.mode);
|
||||||
|
|
||||||
|
// Check hashOfSecretA was extracted correctly
|
||||||
|
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||||
|
|
||||||
|
// Check trade partner/recipient Qortal address was extracted correctly
|
||||||
|
assertEquals(recipient.getAddress(), tradeData.qortalRecipient);
|
||||||
|
|
||||||
|
// Check trade partner/recipient's Bitcoin PKH was extracted correctly
|
||||||
|
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.recipientBitcoinPKH));
|
||||||
|
|
||||||
|
// Test orphaning
|
||||||
|
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||||
|
|
||||||
|
long expectedBalance = deployersPostDeploymentBalance;
|
||||||
|
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@Test
|
||||||
public void testIncorrectTradeSender() throws DataException {
|
public void testIncorrectTradeSender() throws DataException {
|
||||||
@ -171,11 +230,10 @@ public class AtTests extends Common {
|
|||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
String atAddress = at.getAddress();
|
||||||
|
|
||||||
// Send recipient's address to AT BUT NOT FROM AT CREATOR
|
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA);
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||||
|
|
||||||
// Initial payment should NOT happen
|
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance;
|
long expectedBalance = recipientsInitialBalance;
|
||||||
@ -184,6 +242,12 @@ public class AtTests extends Common {
|
|||||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
describeAt(repository, atAddress);
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||||
|
|
||||||
|
// AT should still be in OFFER mode
|
||||||
|
assertEquals(CrossChainTradeData.Mode.OFFER, tradeData.mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,12 +265,12 @@ public class AtTests extends Common {
|
|||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
String atAddress = at.getAddress();
|
||||||
|
|
||||||
// Send recipient's address to AT
|
// Send trade info to AT
|
||||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA);
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
|
|
||||||
// Initial payment should happen 1st block after receiving recipient address
|
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||||
BlockUtils.mintBlock(repository);
|
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||||
|
|
||||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||||
@ -216,9 +280,12 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
describeAt(repository, atAddress);
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
// Check AT is finished
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
assertTrue(atData.getIsFinished());
|
||||||
|
|
||||||
// Test orphaning
|
// Test orphaning
|
||||||
BlockUtils.orphanLastBlock(repository);
|
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||||
BlockUtils.orphanLastBlock(repository);
|
|
||||||
|
|
||||||
long expectedBalance = deployersPostDeploymentBalance;
|
long expectedBalance = deployersPostDeploymentBalance;
|
||||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||||
@ -229,7 +296,7 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@Test
|
||||||
public void testCorrectSecretCorrectSender() throws DataException {
|
public void testCorrectSecretsCorrectSender() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||||
@ -241,27 +308,32 @@ public class AtTests extends Common {
|
|||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
String atAddress = at.getAddress();
|
||||||
|
|
||||||
// Send recipient's address to AT
|
// Send trade info to AT
|
||||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA);
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
|
|
||||||
// Initial payment should happen 1st block after receiving recipient address
|
// Give AT time to process message
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
// Send correct secret to AT
|
// Send correct secrets to AT, from correct account
|
||||||
messageTransaction = sendMessage(repository, recipient, secret, atAddress);
|
messageData = BTCACCT.buildTradeMessage(secretA, secretB);
|
||||||
|
messageTransaction = sendMessage(repository, recipient, messageData, atAddress);
|
||||||
|
|
||||||
// AT should send funds in the next block
|
// AT should send funds in the next block
|
||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
// Check AT is finished
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
assertTrue(atData.getIsFinished());
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
describeAt(repository, atAddress);
|
|
||||||
|
|
||||||
// Orphan redeem
|
// Orphan redeem
|
||||||
BlockUtils.orphanLastBlock(repository);
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
@ -279,7 +351,7 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@Test
|
||||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
public void testCorrectSecretsIncorrectSender() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||||
@ -294,34 +366,39 @@ public class AtTests extends Common {
|
|||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
String atAddress = at.getAddress();
|
||||||
|
|
||||||
// Send recipient's address to AT
|
// Send trade info to AT
|
||||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA);
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
|
|
||||||
// Initial payment should happen 1st block after receiving recipient address
|
// Give AT time to process message
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
// Send correct secret to AT, but from wrong account
|
// Send correct secrets to AT, but from wrong account
|
||||||
messageTransaction = sendMessage(repository, bystander, secret, atAddress);
|
messageData = BTCACCT.buildTradeMessage(secretA, secretB);
|
||||||
|
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||||
|
|
||||||
// AT should NOT send funds in the next block
|
// AT should NOT send funds in the next block
|
||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
// Check AT is NOT finished
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
assertFalse(atData.getIsFinished());
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance;
|
long expectedBalance = recipientsInitialBalance;
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
describeAt(repository, atAddress);
|
|
||||||
|
|
||||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@Test
|
||||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
public void testIncorrectSecretsCorrectSender() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||||
@ -335,28 +412,53 @@ public class AtTests extends Common {
|
|||||||
Account at = deployAtTransaction.getATAccount();
|
Account at = deployAtTransaction.getATAccount();
|
||||||
String atAddress = at.getAddress();
|
String atAddress = at.getAddress();
|
||||||
|
|
||||||
// Send recipient's address to AT
|
// Send trade info to AT
|
||||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA);
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||||
|
|
||||||
// Initial payment should happen 1st block after receiving recipient address
|
// Give AT time to process message
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
// Send correct secret to AT, but from wrong account
|
// Send incorrect secrets to AT, from correct account
|
||||||
byte[] wrongSecret = Crypto.digest(secret);
|
byte[] wrongSecret = new byte[32];
|
||||||
messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress);
|
Random random = new Random();
|
||||||
|
random.nextBytes(wrongSecret);
|
||||||
|
messageData = BTCACCT.buildTradeMessage(wrongSecret, secretB);
|
||||||
|
messageTransaction = sendMessage(repository, recipient, messageData, atAddress);
|
||||||
|
|
||||||
// AT should NOT send funds in the next block
|
// AT should NOT send funds in the next block
|
||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
// Check AT is NOT finished
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
assertFalse(atData.getIsFinished());
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
|
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
|
// Send incorrect secrets to AT, from correct account
|
||||||
|
messageData = BTCACCT.buildTradeMessage(secretA, wrongSecret);
|
||||||
|
messageTransaction = sendMessage(repository, recipient, messageData, atAddress);
|
||||||
|
|
||||||
|
// AT should NOT send funds in the next block
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
describeAt(repository, atAddress);
|
describeAt(repository, atAddress);
|
||||||
|
|
||||||
|
// Check AT is NOT finished
|
||||||
|
atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
assertFalse(atData.getIsFinished());
|
||||||
|
|
||||||
|
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
|
||||||
|
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
|
|
||||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,7 +498,7 @@ public class AtTests extends Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount);
|
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, tradeTimeout, redeemAmount, bitcoinAmount);
|
||||||
|
|
||||||
long txTimestamp = System.currentTimeMillis();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
byte[] lastReference = deployer.getLastReference();
|
byte[] lastReference = deployer.getLastReference();
|
||||||
@ -455,6 +557,7 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||||
|
int refundTimeout = BTCACCT.calcRefundTimeout(tradeTimeout);
|
||||||
|
|
||||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||||
@ -481,15 +584,17 @@ public class AtTests extends Common {
|
|||||||
+ "\tcreator: %s,\n"
|
+ "\tcreator: %s,\n"
|
||||||
+ "\tcreation timestamp: %s,\n"
|
+ "\tcreation timestamp: %s,\n"
|
||||||
+ "\tcurrent balance: %s QORT,\n"
|
+ "\tcurrent balance: %s QORT,\n"
|
||||||
|
+ "\tis finished: %b,\n"
|
||||||
+ "\tHASH160 of secret-B: %s,\n"
|
+ "\tHASH160 of secret-B: %s,\n"
|
||||||
+ "\tredeem payout: %s QORT,\n"
|
+ "\tredeem payout: %s QORT,\n"
|
||||||
+ "\texpected bitcoin: %s BTC,\n"
|
+ "\texpected bitcoin: %s BTC,\n"
|
||||||
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
+ "\ttrade timeout: %d minutes (from AT creation),\n"
|
||||||
+ "\tcurrent block height: %d,\n",
|
+ "\tcurrent block height: %d,\n",
|
||||||
tradeData.qortalAtAddress,
|
tradeData.qortalAtAddress,
|
||||||
tradeData.qortalCreator,
|
tradeData.qortalCreator,
|
||||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||||
Amounts.prettyAmount(tradeData.qortBalance),
|
Amounts.prettyAmount(tradeData.qortBalance),
|
||||||
|
atData.getIsFinished(),
|
||||||
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
||||||
Amounts.prettyAmount(tradeData.qortAmount),
|
Amounts.prettyAmount(tradeData.qortAmount),
|
||||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||||
@ -503,13 +608,15 @@ public class AtTests extends Common {
|
|||||||
} else {
|
} else {
|
||||||
// Trade
|
// Trade
|
||||||
System.out.println(String.format("\tstatus: 'trade mode',\n"
|
System.out.println(String.format("\tstatus: 'trade mode',\n"
|
||||||
+ "\ttrade timeout: block %d,\n"
|
+ "\trefund height: block %d,\n"
|
||||||
+ "\tHASH160 of secret-A: %s,\n"
|
+ "\tHASH160 of secret-A: %s,\n"
|
||||||
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
|
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
|
||||||
|
+ "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
|
||||||
+ "\ttrade recipient: %s",
|
+ "\ttrade recipient: %s",
|
||||||
tradeData.tradeRefundHeight,
|
tradeData.tradeRefundHeight,
|
||||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||||
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L),
|
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||||
|
tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L),
|
||||||
tradeData.qortalRecipient));
|
tradeData.qortalRecipient));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ public class BtcTests extends Common {
|
|||||||
|
|
||||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
||||||
|
|
||||||
byte[] expectedSecret = AtTests.secret;
|
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||||
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
|
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
|
||||||
|
|
||||||
assertNotNull(secret);
|
assertNotNull(secret);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user