Merge remote-tracking branch 'qortal/master'
# Conflicts: # pom.xml # src/main/java/org/qortal/api/ApiError.java # src/main/java/org/qortal/settings/Settings.java # src/main/resources/i18n/ApiError_en.properties
@ -17,10 +17,10 @@
|
|||||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||||
<ROW Property="NTP_GOOD" Value="false"/>
|
<ROW Property="NTP_GOOD" Value="false"/>
|
||||||
<ROW Property="ProductCode" Value="1033:{EB5562C3-664E-4A8B-8907-6D2033B98836} 1049:{36D0E774-B970-4A13-BCC4-1BA6AB3B2633} 2052:{AF6B6B44-9404-403A-B00F-B7110C28E453} 2057:{68BB9EB8-5991-42E5-841C-E76ACE51166D} " Type="16"/>
|
<ROW Property="ProductCode" Value="1033:{E67F58ED-2236-43A9-8895-B9AB96C60EB9} 1049:{E612D7B8-EB9F-481C-81FF-7530E0801F95} 2052:{8EC6F665-1D21-4DB8-8C55-05C2550FF1B3} 2057:{6CE725B6-BBDD-459D-8016-6D1D2FC1F4EC} " Type="16"/>
|
||||||
<ROW Property="ProductLanguage" Value="2057"/>
|
<ROW Property="ProductLanguage" Value="2057"/>
|
||||||
<ROW Property="ProductName" Value="Qortal"/>
|
<ROW Property="ProductName" Value="Qortal"/>
|
||||||
<ROW Property="ProductVersion" Value="1.5.4" Type="32"/>
|
<ROW Property="ProductVersion" Value="1.5.6" Type="32"/>
|
||||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
@ -212,7 +212,7 @@
|
|||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||||
<ROW Component="AI_CustomARPName" ComponentId="{83DFE721-3F68-4ABE-8697-8EC3A91EEB8A}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
<ROW Component="AI_CustomARPName" ComponentId="{9DA2985C-778C-4D85-A44E-5B00D935EED2}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||||
|
@ -131,9 +131,12 @@ public enum ApiError {
|
|||||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE(1202, 402),
|
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE(1202, 402),
|
||||||
FOREIGN_BLOCKCHAIN_TOO_SOON(1203, 408),
|
FOREIGN_BLOCKCHAIN_TOO_SOON(1203, 408),
|
||||||
|
|
||||||
|
// Trade portal
|
||||||
|
ORDER_SIZE_TOO_SMALL(1300, 402);
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
FILE_NOT_FOUND(1301, 404),
|
FILE_NOT_FOUND(1401, 404),
|
||||||
NO_REPLY(1302, 404);
|
NO_REPLY(1402, 404);
|
||||||
|
|
||||||
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
|
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
|
||||||
|
|
||||||
|
@ -25,6 +25,12 @@ public class CrossChainTradeSummary {
|
|||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
private long foreignAmount;
|
private long foreignAmount;
|
||||||
|
|
||||||
|
private String atAddress;
|
||||||
|
|
||||||
|
private String sellerAddress;
|
||||||
|
|
||||||
|
private String buyerReceivingAddress;
|
||||||
|
|
||||||
protected CrossChainTradeSummary() {
|
protected CrossChainTradeSummary() {
|
||||||
/* For JAXB */
|
/* For JAXB */
|
||||||
}
|
}
|
||||||
@ -34,6 +40,9 @@ public class CrossChainTradeSummary {
|
|||||||
this.qortAmount = crossChainTradeData.qortAmount;
|
this.qortAmount = crossChainTradeData.qortAmount;
|
||||||
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
this.foreignAmount = crossChainTradeData.expectedForeignAmount;
|
||||||
this.btcAmount = this.foreignAmount;
|
this.btcAmount = this.foreignAmount;
|
||||||
|
this.sellerAddress = crossChainTradeData.qortalCreator;
|
||||||
|
this.buyerReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
|
||||||
|
this.atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTradeTimestamp() {
|
public long getTradeTimestamp() {
|
||||||
@ -48,7 +57,11 @@ public class CrossChainTradeSummary {
|
|||||||
return this.btcAmount;
|
return this.btcAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getForeignAmount() {
|
public long getForeignAmount() { return this.foreignAmount; }
|
||||||
return this.foreignAmount;
|
|
||||||
}
|
public String getAtAddress() { return this.atAddress; }
|
||||||
|
|
||||||
|
public String getSellerAddress() { return this.sellerAddress; }
|
||||||
|
|
||||||
|
public String getBuyerReceivingAddressAddress() { return this.buyerReceivingAddress; }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package org.qortal.api.model.crosschain;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class DogecoinSendRequest {
|
||||||
|
|
||||||
|
@Schema(description = "Dogecoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||||
|
public String xprv58;
|
||||||
|
|
||||||
|
@Schema(description = "Recipient's Dogecoin address ('legacy' P2PKH only)", example = "DoGecoinEaterAddressDontSendhLfzKD")
|
||||||
|
public String receivingAddress;
|
||||||
|
|
||||||
|
@Schema(description = "Amount of DOGE to send", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public long dogecoinAmount;
|
||||||
|
|
||||||
|
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DOGE (100 sats) per byte", example = "0.00000100", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public Long feePerByte;
|
||||||
|
|
||||||
|
public DogecoinSendRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
package org.qortal.api.resource;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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 org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.api.ApiError;
|
||||||
|
import org.qortal.api.ApiErrors;
|
||||||
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.api.model.CrossChainSecretRequest;
|
||||||
|
import org.qortal.crosschain.AcctMode;
|
||||||
|
import org.qortal.crosschain.DogecoinACCTv1;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.transaction.MessageTransaction;
|
||||||
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.transform.Transformer;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Path("/crosschain/DogecoinACCTv1")
|
||||||
|
@Tag(name = "Cross-Chain (DogecoinACCTv1)")
|
||||||
|
public class CrossChainDogecoinACCTv1Resource {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/redeemmessage")
|
||||||
|
@Operation(
|
||||||
|
summary = "Signs and broadcasts a 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
|
||||||
|
description = "Specify address of cross-chain AT that needs to be messaged, Alice's trade private key, the 32-byte secret,<br>"
|
||||||
|
+ "and an address for receiving QORT from AT. All of these can be found in Alice's trade bot data.<br>"
|
||||||
|
+ "AT needs to be in 'trade' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send!<br>"
|
||||||
|
+ "You need to use the private key that the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = CrossChainSecretRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public boolean buildRedeemMessage(CrossChainSecretRequest secretRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
byte[] partnerPrivateKey = secretRequest.partnerPrivateKey;
|
||||||
|
|
||||||
|
if (partnerPrivateKey == null || partnerPrivateKey.length != Transformer.PRIVATE_KEY_LENGTH)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
if (secretRequest.secret == null || secretRequest.secret.length != DogecoinACCTv1.SECRET_LENGTH)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
|
||||||
|
CrossChainTradeData crossChainTradeData = DogecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||||
|
|
||||||
|
if (crossChainTradeData.mode != AcctMode.TRADING)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
byte[] partnerPublicKey = new PrivateKeyAccount(null, partnerPrivateKey).getPublicKey();
|
||||||
|
String partnerAddress = Crypto.toAddress(partnerPublicKey);
|
||||||
|
|
||||||
|
// MESSAGE must come from address that AT considers trade partner
|
||||||
|
if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
// Good to make MESSAGE
|
||||||
|
|
||||||
|
byte[] messageData = DogecoinACCTv1.buildRedeemMessage(secretRequest.secret, secretRequest.receivingAddress);
|
||||||
|
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, partnerPrivateKey);
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, secretRequest.atAddress, messageData, false, false);
|
||||||
|
|
||||||
|
messageTransaction.computeNonce();
|
||||||
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
if (atData == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
|
|
||||||
|
// Must be correct AT - check functionality using code hash
|
||||||
|
if (!Arrays.equals(atData.getCodeHash(), DogecoinACCTv1.CODE_BYTES_HASH))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// No point sending message to AT that's finished
|
||||||
|
if (atData.getIsFinished())
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
return atData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
package org.qortal.api.resource;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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 org.bitcoinj.core.Transaction;
|
||||||
|
import org.qortal.api.ApiError;
|
||||||
|
import org.qortal.api.ApiErrors;
|
||||||
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.api.model.crosschain.DogecoinSendRequest;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.Dogecoin;
|
||||||
|
import org.qortal.crosschain.SimpleTransaction;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Path("/crosschain/doge")
|
||||||
|
@Tag(name = "Cross-Chain (Dogecoin)")
|
||||||
|
public class CrossChainDogecoinResource {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/walletbalance")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns DOGE balance for hierarchical, deterministic BIP32 wallet",
|
||||||
|
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "BIP32 'm' private/public key in base58",
|
||||||
|
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
public String getDogecoinWalletBalance(String key58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
if (!dogecoin.isValidDeterministicKey(key58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
Long balance = dogecoin.getWalletBalance(key58);
|
||||||
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
return balance.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/wallettransactions")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
|
||||||
|
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "BIP32 'm' private/public key in base58",
|
||||||
|
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
public List<SimpleTransaction> getDogecoinWalletTransactions(String key58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
if (!dogecoin.isValidDeterministicKey(key58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return dogecoin.getWalletTransactions(key58);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/send")
|
||||||
|
@Operation(
|
||||||
|
summary = "Sends DOGE from hierarchical, deterministic BIP32 wallet to specific address",
|
||||||
|
description = "Currently only supports 'legacy' P2PKH Dogecoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = DogecoinSendRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
public String sendBitcoin(DogecoinSendRequest dogecoinSendRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
if (dogecoinSendRequest.dogecoinAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
if (dogecoinSendRequest.feePerByte != null && dogecoinSendRequest.feePerByte <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
if (!dogecoin.isValidAddress(dogecoinSendRequest.receivingAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
if (!dogecoin.isValidDeterministicKey(dogecoinSendRequest.xprv58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
Transaction spendTransaction = dogecoin.buildSpend(dogecoinSendRequest.xprv58,
|
||||||
|
dogecoinSendRequest.receivingAddress,
|
||||||
|
dogecoinSendRequest.dogecoinAmount,
|
||||||
|
dogecoinSendRequest.feePerByte);
|
||||||
|
|
||||||
|
if (spendTransaction == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
dogecoin.broadcastTransaction(spendTransaction);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spendTransaction.getTxId().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -46,7 +46,7 @@ public class CrossChainHtlcResource {
|
|||||||
@Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
@Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Returns HTLC address based on trade info",
|
summary = "Returns HTLC address based on trade info",
|
||||||
description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
description = "Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||||
@ -96,7 +96,7 @@ public class CrossChainHtlcResource {
|
|||||||
@Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
@Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks HTLC status",
|
summary = "Checks HTLC status",
|
||||||
description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
description = "Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
|
||||||
responses = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
|
||||||
@ -174,55 +174,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress}")
|
@Path("/redeem/{ataddress}")
|
||||||
@Operation(
|
|
||||||
summary = "Redeems HTLC associated with supplied AT, using private key, secret, and receiving address",
|
|
||||||
description = "Secret and private key should be 32 bytes (base58 encoded). Receiving address must be a valid LTC P2PKH address.<br>" +
|
|
||||||
"The secret can be found in Alice's trade bot data or in the message to Bob's AT.<br>" +
|
|
||||||
"The trade private key and receiving address can be found in Bob's trade bot data.",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
|
|
||||||
public boolean redeemHtlc(@PathParam("ataddress") String atAddress,
|
|
||||||
@PathParam("tradePrivateKey") String tradePrivateKey,
|
|
||||||
@PathParam("secret") String secret,
|
|
||||||
@PathParam("receivingAddress") String receivingAddress) {
|
|
||||||
Security.checkApiCallAllowed(request);
|
|
||||||
|
|
||||||
// base58 decode the trade private key
|
|
||||||
byte[] decodedTradePrivateKey = null;
|
|
||||||
if (tradePrivateKey != null)
|
|
||||||
decodedTradePrivateKey = Base58.decode(tradePrivateKey);
|
|
||||||
|
|
||||||
// base58 decode the secret
|
|
||||||
byte[] decodedSecret = null;
|
|
||||||
if (secret != null)
|
|
||||||
decodedSecret = Base58.decode(secret);
|
|
||||||
|
|
||||||
// Convert supplied Litecoin receiving address into public key hash (we only support P2PKH at this time)
|
|
||||||
Address litecoinReceivingAddress;
|
|
||||||
try {
|
|
||||||
litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), receivingAddress);
|
|
||||||
} catch (AddressFormatException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
}
|
|
||||||
if (litecoinReceivingAddress.getOutputScriptType() != Script.ScriptType.P2PKH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
|
|
||||||
|
|
||||||
return this.doRedeemHtlc(atAddress, decodedTradePrivateKey, decodedSecret, litecoinReceivingAccountInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/redeem/LITECOIN/{ataddress}")
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redeems HTLC associated with supplied AT",
|
summary = "Redeems HTLC associated with supplied AT",
|
||||||
description = "To be used by a QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in a P2SH.<br>" +
|
description = "To be used by a QORT seller (Bob) who needs to redeem LTC/DOGE/etc proceeds that are stuck in a P2SH.<br>" +
|
||||||
"This requires Bob's trade bot data to be present in the database for this AT.<br>" +
|
"This requires Bob's trade bot data to be present in the database for this AT.<br>" +
|
||||||
"It will fail if the buyer has yet to redeem the QORT held in the AT.",
|
"It will fail if the buyer has yet to redeem the QORT held in the AT.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -249,7 +204,7 @@ public class CrossChainHtlcResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Attempt to find secret from the buyer's message to AT
|
// Attempt to find secret from the buyer's message to AT
|
||||||
byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] decodedSecret = acct.findSecretA(repository, crossChainTradeData);
|
||||||
if (decodedSecret == null) {
|
if (decodedSecret == null) {
|
||||||
LOGGER.info(() -> String.format("Unable to find secret-A from redeem message to AT %s", atAddress));
|
LOGGER.info(() -> String.format("Unable to find secret-A from redeem message to AT %s", atAddress));
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
@ -263,13 +218,13 @@ public class CrossChainHtlcResource {
|
|||||||
if (tradeBotData != null)
|
if (tradeBotData != null)
|
||||||
decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
||||||
|
|
||||||
// Search for the litecoin receiving address in the tradebot data
|
// Search for the foreign blockchain receiving address in the tradebot data
|
||||||
byte[] litecoinReceivingAccountInfo = null;
|
byte[] foreignBlockchainReceivingAccountInfo = null;
|
||||||
if (tradeBotData != null)
|
if (tradeBotData != null)
|
||||||
// Use receiving address PKH from tradebot data
|
// Use receiving address PKH from tradebot data
|
||||||
litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
foreignBlockchainReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
|
||||||
return this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
|
return this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
@ -277,10 +232,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/redeemAll/LITECOIN")
|
@Path("/redeemAll")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redeems HTLC for all applicable ATs in tradebot data",
|
summary = "Redeems HTLC for all applicable ATs in tradebot data",
|
||||||
description = "To be used by a QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in P2SH transactions.<br>" +
|
description = "To be used by a QORT seller (Bob) who needs to redeem LTC/DOGE/etc proceeds that are stuck in P2SH transactions.<br>" +
|
||||||
"This requires Bob's trade bot data to be present in the database for any ATs that need redeeming.<br>" +
|
"This requires Bob's trade bot data to be present in the database for any ATs that need redeeming.<br>" +
|
||||||
"Returns true if at least one trade is redeemed. More detail is available in the log.txt.* file.",
|
"Returns true if at least one trade is redeemed. More detail is available in the log.txt.* file.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -333,7 +288,7 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to find secret from the buyer's message to AT
|
// Attempt to find secret from the buyer's message to AT
|
||||||
byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] decodedSecret = acct.findSecretA(repository, crossChainTradeData);
|
||||||
if (decodedSecret == null) {
|
if (decodedSecret == null) {
|
||||||
LOGGER.info("Unable to find secret-A from redeem message to AT {}", atAddress);
|
LOGGER.info("Unable to find secret-A from redeem message to AT {}", atAddress);
|
||||||
continue;
|
continue;
|
||||||
@ -342,12 +297,12 @@ public class CrossChainHtlcResource {
|
|||||||
// Search for the tradePrivateKey in the tradebot data
|
// Search for the tradePrivateKey in the tradebot data
|
||||||
byte[] decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
byte[] decodedPrivateKey = tradeBotData.getTradePrivateKey();
|
||||||
|
|
||||||
// Search for the litecoin receiving address PKH in the tradebot data
|
// Search for the foreign blockchain receiving address PKH in the tradebot data
|
||||||
byte[] litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
byte[] foreignBlockchainReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LOGGER.info("Attempting to redeem P2SH balance associated with AT {}...", atAddress);
|
LOGGER.info("Attempting to redeem P2SH balance associated with AT {}...", atAddress);
|
||||||
boolean redeemed = this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
|
boolean redeemed = this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||||
if (redeemed) {
|
if (redeemed) {
|
||||||
LOGGER.info("Redeemed P2SH balance associated with AT {}", atAddress);
|
LOGGER.info("Redeemed P2SH balance associated with AT {}", atAddress);
|
||||||
success = true;
|
success = true;
|
||||||
@ -367,8 +322,10 @@ public class CrossChainHtlcResource {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doRedeemHtlc(String atAddress, byte[] decodedTradePrivateKey, byte[] decodedSecret, byte[] litecoinReceivingAccountInfo) {
|
private boolean doRedeemHtlc(String atAddress, byte[] decodedTradePrivateKey, byte[] decodedSecret,
|
||||||
|
byte[] foreignBlockchainReceivingAccountInfo) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
@ -390,30 +347,34 @@ public class CrossChainHtlcResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Validate receiving address
|
// Validate receiving address
|
||||||
if (litecoinReceivingAccountInfo == null || litecoinReceivingAccountInfo.length != 20)
|
if (foreignBlockchainReceivingAccountInfo == null || foreignBlockchainReceivingAccountInfo.length != 20)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Make sure the receiving address isn't a QORT address, given that we can share the same field for both QORT and LTC
|
// Make sure the receiving address isn't a QORT address, given that we can share the same field for both QORT and foreign blockchains
|
||||||
if (Crypto.isValidAddress(litecoinReceivingAccountInfo))
|
if (Crypto.isValidAddress(foreignBlockchainReceivingAccountInfo))
|
||||||
if (Base58.encode(litecoinReceivingAccountInfo).startsWith("Q"))
|
if (Base58.encode(foreignBlockchainReceivingAccountInfo).startsWith("Q"))
|
||||||
// This is likely a QORT address, not an LTC
|
// This is likely a QORT address, not a foreign blockchain
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
|
||||||
// Use secret-A to redeem P2SH-A
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
if (bitcoiny.getClass() == Bitcoin.class) {
|
||||||
|
LOGGER.info("Redeeming a Bitcoin HTLC is not yet supported");
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
}
|
||||||
|
|
||||||
int lockTime = crossChainTradeData.lockTimeA;
|
int lockTime = crossChainTradeData.lockTimeA;
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||||
LOGGER.info(String.format("Redeeming P2SH address: %s", p2shAddressA));
|
LOGGER.info(String.format("Redeeming P2SH address: %s", p2shAddressA));
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -434,13 +395,14 @@ public class CrossChainHtlcResource {
|
|||||||
case FUNDED: {
|
case FUNDED: {
|
||||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
|
ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
|
||||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey,
|
||||||
fundingOutputs, redeemScriptA, decodedSecret, litecoinReceivingAccountInfo);
|
fundingOutputs, redeemScriptA, decodedSecret, foreignBlockchainReceivingAccountInfo);
|
||||||
|
|
||||||
litecoin.broadcastTransaction(p2shRedeemTransaction);
|
bitcoiny.broadcastTransaction(p2shRedeemTransaction);
|
||||||
return true; // TODO: validate?
|
LOGGER.info(String.format("P2SH address %s redeemed!", p2shAddressA));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,10 +416,10 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/refund/LITECOIN/{ataddress}")
|
@Path("/refund/{ataddress}")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Refunds HTLC associated with supplied AT",
|
summary = "Refunds HTLC associated with supplied AT",
|
||||||
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH.<br>" +
|
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC/DOGE/etc that is stuck in a P2SH.<br>" +
|
||||||
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
||||||
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -479,9 +441,17 @@ public class CrossChainHtlcResource {
|
|||||||
if (tradeBotData.getForeignKey() == null)
|
if (tradeBotData.getForeignKey() == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// Determine LTC receive address for refund
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
if (atData == null)
|
||||||
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
|
|
||||||
|
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
|
||||||
|
if (acct == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// Determine foreign blockchain receive address for refund
|
||||||
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
String receiveAddress = bitcoiny.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||||
|
|
||||||
return this.doRefundHtlc(atAddress, receiveAddress);
|
return this.doRefundHtlc(atAddress, receiveAddress);
|
||||||
|
|
||||||
@ -492,11 +462,12 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/refund/LITECOIN/{ataddress}/{receivingAddress}")
|
@Path("/refundAll")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Refunds HTLC associated with supplied AT, to the specified LTC receiving address",
|
summary = "Refunds HTLC for all applicable ATs in tradebot data",
|
||||||
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH.<br>" +
|
description = "To be used by a QORT buyer (Alice) who needs to refund their LTC/DOGE/etc proceeds that are stuck in P2SH transactions.<br>" +
|
||||||
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
"This requires Alice's trade bot data to be present in the database for this AT.<br>" +
|
||||||
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
"It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
|
||||||
responses = {
|
responses = {
|
||||||
@ -506,15 +477,85 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
|
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
|
||||||
public boolean refundHtlc(@PathParam("ataddress") String atAddress,
|
public boolean refundAllHtlc() {
|
||||||
@PathParam("receivingAddress") String receivingAddress) {
|
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
return this.doRefundHtlc(atAddress, receivingAddress);
|
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
boolean success = false;
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||||
|
|
||||||
|
for (TradeBotData tradeBotData : allTradeBotData) {
|
||||||
|
String atAddress = tradeBotData.getAtAddress();
|
||||||
|
if (atAddress == null) {
|
||||||
|
LOGGER.info("Missing AT address in tradebot data", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tradeState = tradeBotData.getState();
|
||||||
|
if (tradeState == null) {
|
||||||
|
LOGGER.info("Missing trade state for AT {}", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tradeState.startsWith("BOB")) {
|
||||||
|
LOGGER.info("AT {} isn't refundable because it is a sell order", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
|
if (atData == null) {
|
||||||
|
LOGGER.info("Couldn't find AT with address {}", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
|
||||||
|
if (acct == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||||
|
if (crossChainTradeData == null) {
|
||||||
|
LOGGER.info("Couldn't find crosschain trade data for AT {}", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tradeBotData.getForeignKey() == null) {
|
||||||
|
LOGGER.info("Couldn't find foreign key for AT {}", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine foreign blockchain receive address for refund
|
||||||
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
String receivingAddress = bitcoiny.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||||
|
|
||||||
|
LOGGER.info("Attempting to refund P2SH balance associated with AT {}...", atAddress);
|
||||||
|
boolean refunded = this.doRefundHtlc(atAddress, receivingAddress);
|
||||||
|
if (refunded) {
|
||||||
|
LOGGER.info("Refunded P2SH balance associated with AT {}", atAddress);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.info("Couldn't refund P2SH balance associated with AT {}. Already redeemed?", atAddress);
|
||||||
|
}
|
||||||
|
} catch (ApiException | ForeignBlockchainException e) {
|
||||||
|
LOGGER.info("Couldn't refund P2SH balance associated with AT {}. Missing data?", atAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean doRefundHtlc(String atAddress, String receiveAddress) {
|
private boolean doRefundHtlc(String atAddress, String receiveAddress) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||||
if (atData == null)
|
if (atData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
@ -532,6 +573,11 @@ public class CrossChainHtlcResource {
|
|||||||
if (tradeBotData == null)
|
if (tradeBotData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
if (bitcoiny.getClass() == Bitcoin.class) {
|
||||||
|
LOGGER.info("Refunding a Bitcoin HTLC is not yet supported");
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
}
|
||||||
|
|
||||||
int lockTime = tradeBotData.getLockTimeA();
|
int lockTime = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
@ -539,22 +585,20 @@ public class CrossChainHtlcResource {
|
|||||||
if (NTP.getTime() <= lockTime * 1000L)
|
if (NTP.getTime() <= lockTime * 1000L)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
|
||||||
|
|
||||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
int medianBlockTime = litecoin.getMedianBlockTime();
|
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||||
if (medianBlockTime <= lockTime)
|
if (medianBlockTime <= lockTime)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||||
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -572,18 +616,18 @@ public class CrossChainHtlcResource {
|
|||||||
case FUNDED:{
|
case FUNDED:{
|
||||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
// Validate the destination LTC address
|
// Validate the destination foreign blockchain address
|
||||||
Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
|
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||||
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
|
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||||
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
||||||
|
|
||||||
litecoin.broadcastTransaction(p2shRefundTransaction);
|
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
||||||
return true; // TODO: validate?
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ public class CrossChainTradeBotResource {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.INSUFFICIENT_BALANCE, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.INSUFFICIENT_BALANCE, ApiError.REPOSITORY_ISSUE, ApiError.ORDER_SIZE_TOO_SMALL})
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
|
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
|
||||||
Security.checkApiCallAllowed(request);
|
Security.checkApiCallAllowed(request);
|
||||||
@ -128,10 +128,13 @@ public class CrossChainTradeBotResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
if (tradeBotCreateRequest.foreignAmount == null || tradeBotCreateRequest.foreignAmount <= 0)
|
if (tradeBotCreateRequest.foreignAmount == null || tradeBotCreateRequest.foreignAmount <= 0)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_SIZE_TOO_SMALL);
|
||||||
|
|
||||||
|
if (tradeBotCreateRequest.foreignAmount < foreignBlockchain.getMinimumOrderAmount())
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_SIZE_TOO_SMALL);
|
||||||
|
|
||||||
if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
|
if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_SIZE_TOO_SMALL);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// Do some simple checking first
|
// Do some simple checking first
|
||||||
|
@ -33,7 +33,6 @@ import org.qortal.transaction.Transaction.TransactionType;
|
|||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
import org.qortal.transform.transaction.TransactionTransformer;
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
import org.qortal.transform.transaction.TransactionTransformer.Transformation;
|
import org.qortal.transform.transaction.TransactionTransformer.Transformation;
|
||||||
import org.qortal.utils.BIP39;
|
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
@ -195,123 +194,6 @@ public class UtilsResource {
|
|||||||
return Base58.encode(random);
|
return Base58.encode(random);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("/mnemonic")
|
|
||||||
@Operation(
|
|
||||||
summary = "Generate 12-word BIP39 mnemonic",
|
|
||||||
description = "Optionally pass 16-byte, base58-encoded entropy or entropy will be internally generated.<br>"
|
|
||||||
+ "Example entropy input: YcVfxkQb6JRzqk5kF2tNLv",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
description = "mnemonic",
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.TEXT_PLAIN,
|
|
||||||
schema = @Schema(
|
|
||||||
type = "string"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA})
|
|
||||||
public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) {
|
|
||||||
if (Settings.getInstance().isApiRestricted())
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
|
|
||||||
* UUID (128bits) and another 4 bits gives 132 bits.
|
|
||||||
* 132 bits, divided by 11, gives 12 words.
|
|
||||||
*/
|
|
||||||
byte[] entropy;
|
|
||||||
if (suppliedEntropy != null) {
|
|
||||||
// Use caller-supplied entropy input
|
|
||||||
try {
|
|
||||||
entropy = Base58.decode(suppliedEntropy);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be 16-bytes
|
|
||||||
if (entropy.length != 16)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
} else {
|
|
||||||
// Generate entropy internally
|
|
||||||
UUID uuid = UUID.randomUUID();
|
|
||||||
|
|
||||||
byte[] uuidMSB = Longs.toByteArray(uuid.getMostSignificantBits());
|
|
||||||
byte[] uuidLSB = Longs.toByteArray(uuid.getLeastSignificantBits());
|
|
||||||
entropy = Bytes.concat(uuidMSB, uuidLSB);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use SHA256 to generate more bits
|
|
||||||
byte[] hash = Crypto.digest(entropy);
|
|
||||||
|
|
||||||
// Append first 4 bits from hash to end. (Actually 8 bits but we only use 4).
|
|
||||||
byte checksum = (byte) (hash[0] & 0xf0);
|
|
||||||
entropy = Bytes.concat(entropy, new byte[] {
|
|
||||||
checksum
|
|
||||||
});
|
|
||||||
|
|
||||||
return BIP39.encode(entropy, "en");
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("/mnemonic")
|
|
||||||
@Operation(
|
|
||||||
summary = "Calculate binary entropy from 12-word BIP39 mnemonic",
|
|
||||||
description = "Returns the base58-encoded binary form, or \"false\" if mnemonic is invalid.",
|
|
||||||
requestBody = @RequestBody(
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.TEXT_PLAIN,
|
|
||||||
schema = @Schema(
|
|
||||||
type = "string"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(
|
|
||||||
description = "entropy in base58",
|
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.TEXT_PLAIN,
|
|
||||||
schema = @Schema(
|
|
||||||
type = "string"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@ApiErrors({ApiError.NON_PRODUCTION})
|
|
||||||
public String fromMnemonic(String mnemonic) {
|
|
||||||
if (Settings.getInstance().isApiRestricted())
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
|
||||||
|
|
||||||
if (mnemonic.isEmpty())
|
|
||||||
return "false";
|
|
||||||
|
|
||||||
// Strip leading/trailing whitespace if any
|
|
||||||
mnemonic = mnemonic.trim();
|
|
||||||
|
|
||||||
String[] phraseWords = mnemonic.split(" ");
|
|
||||||
if (phraseWords.length != 12)
|
|
||||||
return "false";
|
|
||||||
|
|
||||||
// Convert BIP39 mnemonic to binary
|
|
||||||
byte[] binary = BIP39.decode(phraseWords, "en");
|
|
||||||
if (binary == null)
|
|
||||||
return "false";
|
|
||||||
|
|
||||||
byte[] entropy = Arrays.copyOf(binary, 16); // 132 bits is 16.5 bytes, but we're discarding checksum nybble
|
|
||||||
|
|
||||||
byte checksumNybble = (byte) (binary[16] & 0xf0);
|
|
||||||
byte[] checksum = Crypto.digest(entropy);
|
|
||||||
if (checksumNybble != (byte) (checksum[0] & 0xf0))
|
|
||||||
return "false";
|
|
||||||
|
|
||||||
return Base58.encode(entropy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/privatekey")
|
@Path("/privatekey")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -710,6 +710,7 @@ public class Controller extends Thread {
|
|||||||
hasStatusChanged = true;
|
hasStatusChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
peer.setSyncInProgress(true);
|
||||||
|
|
||||||
if (hasStatusChanged)
|
if (hasStatusChanged)
|
||||||
updateSysTray();
|
updateSysTray();
|
||||||
@ -789,6 +790,7 @@ public class Controller extends Thread {
|
|||||||
return syncResult;
|
return syncResult;
|
||||||
} finally {
|
} finally {
|
||||||
isSynchronizing = false;
|
isSynchronizing = false;
|
||||||
|
peer.setSyncInProgress(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -840,6 +842,7 @@ public class Controller extends Thread {
|
|||||||
private void updateSysTray() {
|
private void updateSysTray() {
|
||||||
if (NTP.getTime() == null) {
|
if (NTP.getTime() == null) {
|
||||||
SysTray.getInstance().setToolTipText(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_CLOCK"));
|
SysTray.getInstance().setToolTipText(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_CLOCK"));
|
||||||
|
SysTray.getInstance().setTrayIcon(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -853,14 +856,22 @@ public class Controller extends Thread {
|
|||||||
String actionText;
|
String actionText;
|
||||||
|
|
||||||
synchronized (this.syncLock) {
|
synchronized (this.syncLock) {
|
||||||
if (this.isMintingPossible)
|
if (this.isMintingPossible) {
|
||||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||||
else if (this.isSynchronizing)
|
SysTray.getInstance().setTrayIcon(2);
|
||||||
|
}
|
||||||
|
else if (this.isSynchronizing) {
|
||||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), this.syncPercent);
|
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), this.syncPercent);
|
||||||
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers())
|
SysTray.getInstance().setTrayIcon(3);
|
||||||
|
}
|
||||||
|
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
|
||||||
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
|
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
|
||||||
else
|
SysTray.getInstance().setTrayIcon(3);
|
||||||
|
}
|
||||||
|
else {
|
||||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
||||||
|
SysTray.getInstance().setTrayIcon(4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("Build version: %s", this.buildVersion);
|
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("Build version: %s", this.buildVersion);
|
||||||
|
@ -68,6 +68,9 @@ public class Synchronizer {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Keep track of the size of the last re-org, so it can be logged
|
||||||
|
private int lastReorgSize;
|
||||||
|
|
||||||
private static Synchronizer instance;
|
private static Synchronizer instance;
|
||||||
|
|
||||||
public enum SynchronizationResult {
|
public enum SynchronizationResult {
|
||||||
@ -515,9 +518,22 @@ public class Synchronizer {
|
|||||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||||
|
|
||||||
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
||||||
LOGGER.debug(String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
String syncString = String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
||||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
||||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
|
||||||
|
|
||||||
|
// If our latest block is very old, we should log that we're attempting to sync with a peer
|
||||||
|
// Otherwise, it can appear as though nothing is happening for a while after launch
|
||||||
|
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||||
|
if (minLatestBlockTimestamp != null && ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||||
|
LOGGER.info(syncString);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LOGGER.debug(syncString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset last re-org size as we are starting a new sync round
|
||||||
|
this.lastReorgSize = 0;
|
||||||
|
|
||||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries, true);
|
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries, true);
|
||||||
@ -576,10 +592,19 @@ public class Synchronizer {
|
|||||||
// Commit
|
// Commit
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
|
||||||
|
// Create string for logging
|
||||||
final BlockData newLatestBlockData = repository.getBlockRepository().getLastBlock();
|
final BlockData newLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
LOGGER.info(String.format("Synchronized with peer %s to height %d, sig %.8s, ts: %d", peer,
|
String syncLog = String.format("Synchronized with peer %s to height %d, sig %.8s, ts: %d", peer,
|
||||||
newLatestBlockData.getHeight(), Base58.encode(newLatestBlockData.getSignature()),
|
newLatestBlockData.getHeight(), Base58.encode(newLatestBlockData.getSignature()),
|
||||||
newLatestBlockData.getTimestamp()));
|
newLatestBlockData.getTimestamp());
|
||||||
|
|
||||||
|
// Append re-org info
|
||||||
|
if (this.lastReorgSize > 0) {
|
||||||
|
syncLog = syncLog.concat(String.format(", size: %d", this.lastReorgSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log sync info
|
||||||
|
LOGGER.info(syncLog);
|
||||||
|
|
||||||
return SynchronizationResult.OK;
|
return SynchronizationResult.OK;
|
||||||
} finally {
|
} finally {
|
||||||
@ -933,6 +958,7 @@ public class Synchronizer {
|
|||||||
// Unwind to common block (unless common block is our latest block)
|
// Unwind to common block (unless common block is our latest block)
|
||||||
int ourHeight = ourInitialHeight;
|
int ourHeight = ourInitialHeight;
|
||||||
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s. Our height: %d", commonBlockHeight, commonBlockSig58, ourHeight));
|
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s. Our height: %d", commonBlockHeight, commonBlockSig58, ourHeight));
|
||||||
|
int reorgSize = ourHeight - commonBlockHeight;
|
||||||
|
|
||||||
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourInitialHeight);
|
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourInitialHeight);
|
||||||
while (ourHeight > commonBlockHeight) {
|
while (ourHeight > commonBlockHeight) {
|
||||||
@ -981,6 +1007,7 @@ public class Synchronizer {
|
|||||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.lastReorgSize = reorgSize;
|
||||||
return SynchronizationResult.OK;
|
return SynchronizationResult.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1033,7 +1033,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] secretA = BitcoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] secretA = BitcoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
if (secretA == null) {
|
if (secretA == null) {
|
||||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
|
@ -0,0 +1,883 @@
|
|||||||
|
package org.qortal.controller.tradebot;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bitcoinj.core.*;
|
||||||
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.account.PublicKeyAccount;
|
||||||
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.crosschain.*;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
|
import org.qortal.transaction.MessageTransaction;
|
||||||
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
import static java.util.stream.Collectors.toMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performing cross-chain trading steps on behalf of user.
|
||||||
|
* <p>
|
||||||
|
* We deal with three different independent state-spaces here:
|
||||||
|
* <ul>
|
||||||
|
* <li>Qortal blockchain</li>
|
||||||
|
* <li>Foreign blockchain</li>
|
||||||
|
* <li>Trade-bot entries</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv1TradeBot.class);
|
||||||
|
|
||||||
|
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||||
|
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
||||||
|
BOB_WAITING_FOR_MESSAGE(15, true, true),
|
||||||
|
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
|
||||||
|
BOB_DONE(30, false, false),
|
||||||
|
BOB_REFUNDED(35, false, false),
|
||||||
|
|
||||||
|
ALICE_WAITING_FOR_AT_LOCK(85, true, true),
|
||||||
|
ALICE_DONE(95, false, false),
|
||||||
|
ALICE_REFUNDING_A(105, true, true),
|
||||||
|
ALICE_REFUNDED(110, false, false);
|
||||||
|
|
||||||
|
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||||
|
|
||||||
|
public final int value;
|
||||||
|
public final boolean requiresAtData;
|
||||||
|
public final boolean requiresTradeData;
|
||||||
|
|
||||||
|
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||||
|
this.value = value;
|
||||||
|
this.requiresAtData = requiresAtData;
|
||||||
|
this.requiresTradeData = requiresTradeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static State valueOf(int value) {
|
||||||
|
return map.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getState() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStateValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||||
|
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||||
|
|
||||||
|
private static DogecoinACCTv1TradeBot instance;
|
||||||
|
|
||||||
|
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
|
||||||
|
.map(State::name)
|
||||||
|
.collect(Collectors.toUnmodifiableList());
|
||||||
|
|
||||||
|
private DogecoinACCTv1TradeBot() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DogecoinACCTv1TradeBot getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new DogecoinACCTv1TradeBot();
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getEndStates() {
|
||||||
|
return this.endStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DOGE.
|
||||||
|
* <p>
|
||||||
|
* Generates:
|
||||||
|
* <ul>
|
||||||
|
* <li>new 'trade' private key</li>
|
||||||
|
* </ul>
|
||||||
|
* Derives:
|
||||||
|
* <ul>
|
||||||
|
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
|
||||||
|
* <li>'foreign' (as in Dogecoin) public key, public key hash</li>
|
||||||
|
* </ul>
|
||||||
|
* A Qortal AT is then constructed including the following as constants in the 'data segment':
|
||||||
|
* <ul>
|
||||||
|
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
|
||||||
|
* <li>'foreign'/Dogecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||||
|
* <li>QORT amount on offer by Bob</li>
|
||||||
|
* <li>DOGE amount expected in return by Bob (from Alice)</li>
|
||||||
|
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
|
||||||
|
* </ul>
|
||||||
|
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
|
||||||
|
* <p>
|
||||||
|
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
|
||||||
|
* <p>
|
||||||
|
* @param repository
|
||||||
|
* @param tradeBotCreateRequest
|
||||||
|
* @return raw, unsigned DEPLOY_AT transaction
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
|
||||||
|
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||||
|
|
||||||
|
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||||
|
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||||
|
|
||||||
|
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
|
|
||||||
|
// Convert Dogecoin receiving address into public key hash (we only support P2PKH at this time)
|
||||||
|
Address dogecoinReceivingAddress;
|
||||||
|
try {
|
||||||
|
dogecoinReceivingAddress = Address.fromString(Dogecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||||
|
} catch (AddressFormatException e) {
|
||||||
|
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
}
|
||||||
|
if (dogecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||||
|
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
|
||||||
|
byte[] dogecoinReceivingAccountInfo = dogecoinReceivingAddress.getHash();
|
||||||
|
|
||||||
|
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
|
||||||
|
|
||||||
|
// Deploy AT
|
||||||
|
long timestamp = NTP.getTime();
|
||||||
|
byte[] reference = creator.getLastReference();
|
||||||
|
long fee = 0L;
|
||||||
|
byte[] signature = null;
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
|
||||||
|
|
||||||
|
String name = "QORT/DOGE ACCT";
|
||||||
|
String description = "QORT/DOGE cross-chain trade";
|
||||||
|
String aTType = "ACCT";
|
||||||
|
String tags = "ACCT QORT DOGE";
|
||||||
|
byte[] creationBytes = DogecoinACCTv1.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
|
||||||
|
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
|
||||||
|
long amount = tradeBotCreateRequest.fundingQortAmount;
|
||||||
|
|
||||||
|
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
|
||||||
|
|
||||||
|
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||||
|
fee = deployAtTransaction.calcRecommendedFee();
|
||||||
|
deployAtTransactionData.setFee(fee);
|
||||||
|
|
||||||
|
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
|
||||||
|
String atAddress = deployAtTransactionData.getAtAddress();
|
||||||
|
|
||||||
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv1.NAME,
|
||||||
|
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
|
||||||
|
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
|
||||||
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
|
null, null,
|
||||||
|
SupportedBlockchain.DOGECOIN.name(),
|
||||||
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
tradeBotCreateRequest.foreignAmount, null, null, null, dogecoinReceivingAccountInfo);
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||||
|
|
||||||
|
// Attempt to backup the trade bot data
|
||||||
|
TradeBot.backupTradeBotData(repository);
|
||||||
|
|
||||||
|
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||||
|
try {
|
||||||
|
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DOGE to an existing offer.
|
||||||
|
* <p>
|
||||||
|
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||||
|
* and access to a Dogecoin wallet via <tt>xprv58</tt>.
|
||||||
|
* <p>
|
||||||
|
* The <tt>crossChainTradeData</tt> contains the current trade offer state
|
||||||
|
* as extracted from the AT's data segment.
|
||||||
|
* <p>
|
||||||
|
* Access to a funded wallet is via a Dogecoin BIP32 hierarchical deterministic key,
|
||||||
|
* passed via <tt>xprv58</tt>.
|
||||||
|
* <b>This key will be stored in your node's database</b>
|
||||||
|
* to allow trade-bot to create/fund the necessary P2SH transactions!
|
||||||
|
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot
|
||||||
|
* only a subset of wallet access (see BIP32 for more details).
|
||||||
|
* <p>
|
||||||
|
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
|
||||||
|
* Electrum wallet by going to the console tab and entering:<br>
|
||||||
|
* <tt>wallet.keystore.xprv</tt><br>
|
||||||
|
* which should result in a base58 string starting with either 'xprv' (for Dogecoin main-net)
|
||||||
|
* or 'tprv' for (Dogecoin test-net).
|
||||||
|
* <p>
|
||||||
|
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
|
||||||
|
* <p>
|
||||||
|
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
||||||
|
* with the Dogecoin amount expected by 'Bob'.
|
||||||
|
* <p>
|
||||||
|
* If the Dogecoin transaction is successfully broadcast to the network then
|
||||||
|
* we also send a MESSAGE to Bob's trade-bot to let them know.
|
||||||
|
* <p>
|
||||||
|
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
|
||||||
|
* <p>
|
||||||
|
* @param repository
|
||||||
|
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
|
||||||
|
* @param xprv58 funded wallet xprv in base58
|
||||||
|
* @return true if P2SH-A funding transaction successfully broadcast to Dogecoin network, false otherwise
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
|
||||||
|
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||||
|
byte[] secretA = TradeBot.generateSecret();
|
||||||
|
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||||
|
|
||||||
|
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||||
|
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||||
|
|
||||||
|
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||||
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
|
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||||
|
|
||||||
|
// We need to generate lockTime-A: add tradeTimeout to now
|
||||||
|
long now = NTP.getTime();
|
||||||
|
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
||||||
|
|
||||||
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv1.NAME,
|
||||||
|
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
|
||||||
|
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
||||||
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
|
secretA, hashOfSecretA,
|
||||||
|
SupportedBlockchain.DOGECOIN.name(),
|
||||||
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
||||||
|
|
||||||
|
// Attempt to backup the trade bot data
|
||||||
|
TradeBot.backupTradeBotData(repository);
|
||||||
|
|
||||||
|
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||||
|
long p2shFee;
|
||||||
|
try {
|
||||||
|
p2shFee = Dogecoin.getInstance().getP2shFee(now);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
LOGGER.debug("Couldn't estimate Dogecoin fees?");
|
||||||
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||||
|
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
|
||||||
|
|
||||||
|
// P2SH-A to be funded
|
||||||
|
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||||
|
String p2shAddress = Dogecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||||
|
|
||||||
|
// Build transaction for funding P2SH-A
|
||||||
|
Transaction p2shFundingTransaction = Dogecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||||
|
if (p2shFundingTransaction == null) {
|
||||||
|
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
||||||
|
return ResponseResult.BALANCE_ISSUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Dogecoin.getInstance().broadcastTransaction(p2shFundingTransaction);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
// We couldn't fund P2SH-A at this time
|
||||||
|
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
|
||||||
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||||
|
byte[] messageData = DogecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||||
|
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||||
|
|
||||||
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
|
if (!isMessageAlreadySent) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||||
|
|
||||||
|
messageTransaction.computeNonce();
|
||||||
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||||
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||||
|
|
||||||
|
return ResponseResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||||
|
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||||
|
if (tradeBotState == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If the AT doesn't exist then we might as well let the user tidy up
|
||||||
|
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
switch (tradeBotState) {
|
||||||
|
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||||
|
case ALICE_DONE:
|
||||||
|
case BOB_DONE:
|
||||||
|
case ALICE_REFUNDED:
|
||||||
|
case BOB_REFUNDED:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
|
||||||
|
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||||
|
if (tradeBotState == null) {
|
||||||
|
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ATData atData = null;
|
||||||
|
CrossChainTradeData tradeData = null;
|
||||||
|
|
||||||
|
if (tradeBotState.requiresAtData) {
|
||||||
|
// Attempt to fetch AT data
|
||||||
|
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
||||||
|
if (atData == null) {
|
||||||
|
LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tradeBotState.requiresTradeData) {
|
||||||
|
tradeData = DogecoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||||
|
if (tradeData == null) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tradeBotState) {
|
||||||
|
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||||
|
handleBobWaitingForAtConfirm(repository, tradeBotData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BOB_WAITING_FOR_MESSAGE:
|
||||||
|
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||||
|
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_WAITING_FOR_AT_LOCK:
|
||||||
|
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||||
|
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BOB_WAITING_FOR_AT_REDEEM:
|
||||||
|
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||||
|
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_DONE:
|
||||||
|
case BOB_DONE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_REFUNDING_A:
|
||||||
|
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||||
|
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ALICE_REFUNDED:
|
||||||
|
case BOB_REFUNDED:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for Bob's AT to deploy.
|
||||||
|
* <p>
|
||||||
|
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
|
||||||
|
*/
|
||||||
|
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||||
|
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
|
||||||
|
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We've waited ages for AT to be confirmed into a block but something has gone awry.
|
||||||
|
// After this long we assume transaction loss so give up with trade-bot entry too.
|
||||||
|
tradeBotData.setState(State.BOB_REFUNDED.name());
|
||||||
|
tradeBotData.setStateValue(State.BOB_REFUNDED.value);
|
||||||
|
tradeBotData.setTimestamp(NTP.getTime());
|
||||||
|
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
|
||||||
|
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
|
||||||
|
TradeBot.notifyStateChange(tradeBotData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
|
||||||
|
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
|
||||||
|
* <p>
|
||||||
|
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
|
||||||
|
* in which case trade-bot is done with this specific trade and finalizes on refunded state.
|
||||||
|
* <p>
|
||||||
|
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
|
||||||
|
* <p>
|
||||||
|
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
|
||||||
|
* <p>
|
||||||
|
* Assuming P2SH-A has at least expected Dogecoin balance,
|
||||||
|
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
|
||||||
|
* <p>
|
||||||
|
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
|
||||||
|
* <p>
|
||||||
|
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
|
||||||
|
* extract secret-A needed to redeem Alice's P2SH.
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// If AT has finished then Bob likely cancelled his trade offer
|
||||||
|
if (atData.getIsFinished()) {
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
|
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
String address = tradeBotData.getTradeNativeAddress();
|
||||||
|
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
|
||||||
|
|
||||||
|
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
|
||||||
|
if (messageTransactionData.isText())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// We're expecting: HASH160(secret-A), Alice's Dogecoin pubkeyhash and lockTime-A
|
||||||
|
byte[] messageData = messageTransactionData.getData();
|
||||||
|
DogecoinACCTv1.OfferMessageData offerMessageData = DogecoinACCTv1.extractOfferMessageData(messageData);
|
||||||
|
if (offerMessageData == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDogecoinPKH;
|
||||||
|
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||||
|
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||||
|
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||||
|
int refundTimeout = DogecoinACCTv1.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Determine P2SH-A address and confirm funded
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||||
|
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// We've already redeemed this?
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||||
|
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case FUNDED:
|
||||||
|
// Fall-through out of switch...
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good to go - send MESSAGE to AT
|
||||||
|
|
||||||
|
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
||||||
|
|
||||||
|
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||||
|
byte[] outgoingMessageData = DogecoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||||
|
String messageRecipient = tradeBotData.getAtAddress();
|
||||||
|
|
||||||
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
|
||||||
|
if (!isMessageAlreadySent) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||||
|
|
||||||
|
outgoingMessageTransaction.computeNonce();
|
||||||
|
outgoingMessageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
|
||||||
|
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
|
||||||
|
* <p>
|
||||||
|
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
|
||||||
|
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else
|
||||||
|
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
|
||||||
|
* <p>
|
||||||
|
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
|
||||||
|
* <p>
|
||||||
|
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
|
||||||
|
* <p>
|
||||||
|
* In revealing a valid secret-A, Bob can then redeem the DOGE funds from P2SH-A.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
int lockTimeA = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
|
// Refund P2SH-A if we've passed lockTime-A
|
||||||
|
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
case FUNDED:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Already redeemed?
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||||
|
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||||
|
() -> atData.getIsFinished()
|
||||||
|
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
||||||
|
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're waiting for AT to be in TRADE mode
|
||||||
|
if (crossChainTradeData.mode != AcctMode.TRADING)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
|
||||||
|
|
||||||
|
// Find our MESSAGE to AT from previous state
|
||||||
|
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
|
||||||
|
crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
|
||||||
|
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||||
|
int refundTimeout = DogecoinACCTv1.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Our calculated refundTimeout should match AT's refundTimeout
|
||||||
|
if (refundTimeout != crossChainTradeData.refundTimeout) {
|
||||||
|
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
|
||||||
|
// We'll eventually refund
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're good to redeem AT
|
||||||
|
|
||||||
|
// Send 'redeem' MESSAGE to AT using both secret
|
||||||
|
byte[] secretA = tradeBotData.getSecret();
|
||||||
|
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
||||||
|
byte[] messageData = DogecoinACCTv1.buildRedeemMessage(secretA, qortalReceivingAddress);
|
||||||
|
String messageRecipient = tradeBotData.getAtAddress();
|
||||||
|
|
||||||
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
|
if (!isMessageAlreadySent) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||||
|
|
||||||
|
messageTransaction.computeNonce();
|
||||||
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
|
// Reset repository state to prevent deadlock
|
||||||
|
repository.discardChanges();
|
||||||
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("Redeeming AT %s. Funds should arrive at %s",
|
||||||
|
tradeBotData.getAtAddress(), qortalReceivingAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DOGE funds from P2SH-A.
|
||||||
|
* <p>
|
||||||
|
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
|
||||||
|
* trade-bot is done with this specific trade and finalizes in refunded state.
|
||||||
|
* <p>
|
||||||
|
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DOGE funds from P2SH-A
|
||||||
|
* to Bob's 'foreign'/Dogecoin trade legacy-format address, as derived from trade private key.
|
||||||
|
* <p>
|
||||||
|
* (This could potentially be 'improved' to send DOGE to any address of Bob's choosing by changing the transaction output).
|
||||||
|
* <p>
|
||||||
|
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// AT should be 'finished' once Alice has redeemed QORT funds
|
||||||
|
if (!atData.getIsFinished())
|
||||||
|
// Not finished yet
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If AT is REFUNDED or CANCELLED then something has gone wrong
|
||||||
|
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
|
||||||
|
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DOGE
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
|
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] secretA = DogecoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
|
if (secretA == null) {
|
||||||
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
|
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Double-check that we have redeemed P2SH-A...
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
// Wait for AT to auto-refund
|
||||||
|
return;
|
||||||
|
|
||||||
|
case FUNDED: {
|
||||||
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
|
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
|
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||||
|
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||||
|
|
||||||
|
dogecoin.broadcastTransaction(p2shRedeemTransaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String receivingAddress = dogecoin.pkhToAddress(receivingAccountInfo);
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||||
|
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade-bot is attempting to refund P2SH-A.
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
int lockTimeA = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
|
// We can't refund P2SH-A until lockTime-A has passed
|
||||||
|
if (NTP.getTime() <= lockTimeA * 1000L)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
|
int medianBlockTime = dogecoin.getMedianBlockTime();
|
||||||
|
if (medianBlockTime <= lockTimeA)
|
||||||
|
return;
|
||||||
|
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
|
||||||
|
switch (htlcStatusA) {
|
||||||
|
case UNFUNDED:
|
||||||
|
case FUNDING_IN_PROGRESS:
|
||||||
|
// Still waiting for P2SH-A to be funded...
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REDEEM_IN_PROGRESS:
|
||||||
|
case REDEEMED:
|
||||||
|
// Too late!
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case REFUND_IN_PROGRESS:
|
||||||
|
case REFUNDED:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FUNDED:{
|
||||||
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
|
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
// Determine receive address for refund
|
||||||
|
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||||
|
Address receiving = Address.fromString(dogecoin.getNetworkParameters(), receiveAddress);
|
||||||
|
|
||||||
|
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(dogecoin.getNetworkParameters(), refundAmount, refundKey,
|
||||||
|
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||||
|
|
||||||
|
dogecoin.broadcastTransaction(p2shRefundTransaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||||
|
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
|
||||||
|
* <p>
|
||||||
|
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
|
||||||
|
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||||
|
// This is OK
|
||||||
|
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
|
||||||
|
|
||||||
|
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
|
||||||
|
if (isAtLockedToUs) {
|
||||||
|
// AT is trading with us - OK
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||||
|
() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
|
||||||
|
// We've redeemed already?
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
|
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
|
||||||
|
} else {
|
||||||
|
// Any other state is not good, so start defensive refund
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||||
|
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
|
||||||
|
return (lockTimeA - tradeTimeout * 60) * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -725,7 +725,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] secretA = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
|
byte[] secretA = LitecoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
if (secretA == null) {
|
if (secretA == null) {
|
||||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
|
@ -17,11 +17,7 @@ import org.qortal.account.PrivateKeyAccount;
|
|||||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
||||||
import org.qortal.crosschain.ACCT;
|
import org.qortal.crosschain.*;
|
||||||
import org.qortal.crosschain.BitcoinACCTv1;
|
|
||||||
import org.qortal.crosschain.ForeignBlockchainException;
|
|
||||||
import org.qortal.crosschain.LitecoinACCTv1;
|
|
||||||
import org.qortal.crosschain.SupportedBlockchain;
|
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.crosschain.TradeBotData;
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
@ -80,6 +76,7 @@ public class TradeBot implements Listener {
|
|||||||
static {
|
static {
|
||||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||||
|
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TradeBot instance;
|
private static TradeBot instance;
|
||||||
|
@ -20,4 +20,6 @@ public interface ACCT {
|
|||||||
|
|
||||||
public byte[] buildCancelMessage(String creatorQortalAddress);
|
public byte[] buildCancelMessage(String creatorQortalAddress);
|
||||||
|
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,11 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
new Server("192.166.219.200", Server.ConnectionType.SSL, 50002),
|
new Server("192.166.219.200", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
|
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("dxm.no-ip.biz", Server.ConnectionType.SSL, 50002),
|
new Server("dxm.no-ip.biz", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002));
|
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
|
||||||
|
new Server("ecdsa.net", Server.ConnectionType.SSL, 110),
|
||||||
|
new Server("electrum.hsmiths.com", Server.ConnectionType.SSL, 995),
|
||||||
|
new Server("elec.luggs.co", Server.ConnectionType.SSL, 443),
|
||||||
|
new Server("btc.smsys.me", Server.ConnectionType.SSL, 995));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -872,7 +872,8 @@ public class BitcoinACCTv1 implements ACCT {
|
|||||||
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
|
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
@Override
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||||
|
|
||||||
|
@ -169,6 +169,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
return this.bitcoinjContext.getFeePerKb();
|
return this.bitcoinjContext.getFeePerKb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */
|
||||||
|
public long getMinimumOrderAmount() {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns fixed P2SH spending fee, in sats per 1000bytes, optionally for historic timestamp.
|
* Returns fixed P2SH spending fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||||
*
|
*
|
||||||
@ -346,6 +351,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
||||||
Set<String> keySet = new HashSet<>();
|
Set<String> keySet = new HashSet<>();
|
||||||
|
|
||||||
|
// Set the number of consecutive empty batches required before giving up
|
||||||
|
final int numberOfAdditionalBatchesToSearch = 5;
|
||||||
|
|
||||||
|
int unusedCounter = 0;
|
||||||
int ki = 0;
|
int ki = 0;
|
||||||
do {
|
do {
|
||||||
boolean areAllKeysUnused = true;
|
boolean areAllKeysUnused = true;
|
||||||
@ -369,9 +378,19 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areAllKeysUnused)
|
if (areAllKeysUnused) {
|
||||||
// No transactions for this batch of keys so assume we're done searching.
|
// No transactions
|
||||||
|
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
||||||
|
// ... and we've hit our search limit
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||||
|
unusedCounter++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Some keys in this batch were used, so reset the counter
|
||||||
|
unusedCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate some more keys
|
// Generate some more keys
|
||||||
keys.addAll(generateMoreKeys(keyChain));
|
keys.addAll(generateMoreKeys(keyChain));
|
||||||
|
171
src/main/java/org/qortal/crosschain/Dogecoin.java
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.Context;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
import org.libdohj.params.DogecoinMainNetParams;
|
||||||
|
//import org.libdohj.params.DogecoinRegTestParams;
|
||||||
|
import org.libdohj.params.DogecoinTestNet3Params;
|
||||||
|
import org.qortal.crosschain.ElectrumX.Server;
|
||||||
|
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Dogecoin extends Bitcoiny {
|
||||||
|
|
||||||
|
public static final String CURRENCY_CODE = "DOGE";
|
||||||
|
|
||||||
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(500000000); // 5 DOGE per 1000 bytes
|
||||||
|
|
||||||
|
private static final long MINIMUM_ORDER_AMOUNT = 300000000L; // 3 DOGE minimum order. The RPC dust threshold is around 2 DOGE
|
||||||
|
|
||||||
|
// Temporary values until a dynamic fee system is written.
|
||||||
|
private static final long MAINNET_FEE = 110000000L;
|
||||||
|
private static final long NON_MAINNET_FEE = 10000L; // TODO: calibrate this
|
||||||
|
|
||||||
|
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
||||||
|
static {
|
||||||
|
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
|
||||||
|
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DogecoinNet {
|
||||||
|
MAIN {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return DogecoinMainNetParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
|
new Server("electrum1.cipig.net", ConnectionType.TCP, 10060),
|
||||||
|
new Server("electrum2.cipig.net", ConnectionType.TCP, 10060),
|
||||||
|
new Server("electrum3.cipig.net", ConnectionType.TCP, 10060));
|
||||||
|
// TODO: add more mainnet servers. It's too centralized.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
// TODO: This will need to be replaced with something better in the near future!
|
||||||
|
return MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TEST3 {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return DogecoinTestNet3Params.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(); // TODO: find testnet servers
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
REGTEST {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return null; // TODO: DogecoinRegTestParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Server("localhost", ConnectionType.TCP, 50001),
|
||||||
|
new Server("localhost", ConnectionType.SSL, 50002));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
// This is unique to each regtest instance
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public abstract NetworkParameters getParams();
|
||||||
|
public abstract Collection<Server> getServers();
|
||||||
|
public abstract String getGenesisHash();
|
||||||
|
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dogecoin instance;
|
||||||
|
|
||||||
|
private final DogecoinNet dogecoinNet;
|
||||||
|
|
||||||
|
// Constructors and instance
|
||||||
|
|
||||||
|
private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||||
|
super(blockchain, bitcoinjContext, currencyCode);
|
||||||
|
this.dogecoinNet = dogecoinNet;
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized Dogecoin getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
DogecoinNet dogecoinNet = Settings.getInstance().getDogecoinNet();
|
||||||
|
|
||||||
|
BitcoinyBlockchainProvider electrumX = new ElectrumX("Dogecoin-" + dogecoinNet.name(), dogecoinNet.getGenesisHash(), dogecoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
|
||||||
|
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
||||||
|
|
||||||
|
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters & setters
|
||||||
|
|
||||||
|
public static synchronized void resetForTesting() {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actual useful methods for use by other classes
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Coin getFeePerKb() {
|
||||||
|
return DEFAULT_FEE_PER_KB;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMinimumOrderAmount() {
|
||||||
|
return MINIMUM_ORDER_AMOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns estimated DOGE fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||||
|
*
|
||||||
|
* @param timestamp optional milliseconds since epoch, or null for 'now'
|
||||||
|
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
||||||
|
return this.dogecoinNet.getP2shFee(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
855
src/main/java/org/qortal/crosschain/DogecoinACCTv1.java
Normal file
@ -0,0 +1,855 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.ciyam.at.*;
|
||||||
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.at.QortalFunctionCode;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.at.ATStateData;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.ciyam.at.OpCode.calcOffset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-chain trade AT
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Bob generates Dogecoin & Qortal 'trade' keys
|
||||||
|
* <ul>
|
||||||
|
* <li>private key required to sign P2SH redeem tx</li>
|
||||||
|
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
|
||||||
|
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Bob deploys Qortal AT
|
||||||
|
* <ul>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Alice finds Qortal AT and wants to trade
|
||||||
|
* <ul>
|
||||||
|
* <li>Alice generates Dogecoin & Qortal 'trade' keys</li>
|
||||||
|
* <li>Alice funds Dogecoin P2SH-A</li>
|
||||||
|
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
|
||||||
|
* <ul>
|
||||||
|
* <li>hash-of-secret-A</li>
|
||||||
|
* <li>her 'trade' Dogecoin PKH</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Bob receives "offer" MESSAGE
|
||||||
|
* <ul>
|
||||||
|
* <li>Checks Alice's P2SH-A</li>
|
||||||
|
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
|
||||||
|
* <ul>
|
||||||
|
* <li>Alice's trade Qortal address</li>
|
||||||
|
* <li>Alice's trade Dogecoin PKH</li>
|
||||||
|
* <li>hash-of-secret-A</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Alice checks Qortal AT to confirm it's locked to her
|
||||||
|
* <ul>
|
||||||
|
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
|
||||||
|
* <ul>
|
||||||
|
* <li>secret-A</li>
|
||||||
|
* <li>Qortal receiving address of her chosing</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>AT's QORT funds are sent to Qortal receiving address</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Bob checks AT, extracts secret-A
|
||||||
|
* <ul>
|
||||||
|
* <li>Bob redeems P2SH-A using his Dogecoin trade key and secret-A</li>
|
||||||
|
* <li>P2SH-A DOGE funds end up at Dogecoin address determined by redeem transaction output(s)</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class DogecoinACCTv1 implements ACCT {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv1.class);
|
||||||
|
|
||||||
|
public static final String NAME = DogecoinACCTv1.class.getSimpleName();
|
||||||
|
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("0eb49b0313ff3855a29d860c2a8203faa2ef62e28ea30459321f176079cfa3a5").asBytes(); // SHA256 of AT code bytes
|
||||||
|
|
||||||
|
public static final int SECRET_LENGTH = 32;
|
||||||
|
|
||||||
|
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
|
||||||
|
private static final int MODE_VALUE_OFFSET = 61;
|
||||||
|
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
|
||||||
|
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
|
||||||
|
|
||||||
|
public static class OfferMessageData {
|
||||||
|
public byte[] partnerDogecoinPKH;
|
||||||
|
public byte[] hashOfSecretA;
|
||||||
|
public long lockTimeA;
|
||||||
|
}
|
||||||
|
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDogecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
||||||
|
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
||||||
|
+ 24 /*partner's Dogecoin PKH (padded from 20 to 24)*/
|
||||||
|
+ 8 /*AT trade timeout (minutes)*/
|
||||||
|
+ 24 /*hash of secret-A (padded from 20 to 24)*/
|
||||||
|
+ 8 /*lockTimeA*/;
|
||||||
|
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
|
||||||
|
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
|
||||||
|
|
||||||
|
private static DogecoinACCTv1 instance;
|
||||||
|
|
||||||
|
private DogecoinACCTv1() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DogecoinACCTv1 getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new DogecoinACCTv1();
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getCodeBytesHash() {
|
||||||
|
return CODE_BYTES_HASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getModeByteOffset() {
|
||||||
|
return MODE_BYTE_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ForeignBlockchain getBlockchain() {
|
||||||
|
return Dogecoin.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Qortal AT creation bytes for cross-chain trading AT.
|
||||||
|
* <p>
|
||||||
|
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the
|
||||||
|
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
|
||||||
|
*
|
||||||
|
* @param creatorTradeAddress AT creator's trade Qortal address
|
||||||
|
* @param dogecoinPublicKeyHash 20-byte HASH160 of creator's trade Dogecoin public key
|
||||||
|
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||||
|
* @param dogecoinAmount how much DOGE the AT creator is expecting to trade
|
||||||
|
* @param tradeTimeout suggested timeout for entire trade
|
||||||
|
*/
|
||||||
|
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] dogecoinPublicKeyHash, long qortAmount, long dogecoinAmount, int tradeTimeout) {
|
||||||
|
if (dogecoinPublicKeyHash.length != 20)
|
||||||
|
throw new IllegalArgumentException("Dogecoin public key hash should be 20 bytes");
|
||||||
|
|
||||||
|
// Labels for data segment addresses
|
||||||
|
int addrCounter = 0;
|
||||||
|
|
||||||
|
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
||||||
|
|
||||||
|
final int addrCreatorTradeAddress1 = addrCounter++;
|
||||||
|
final int addrCreatorTradeAddress2 = addrCounter++;
|
||||||
|
final int addrCreatorTradeAddress3 = addrCounter++;
|
||||||
|
final int addrCreatorTradeAddress4 = addrCounter++;
|
||||||
|
|
||||||
|
final int addrDogecoinPublicKeyHash = addrCounter;
|
||||||
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrQortAmount = addrCounter++;
|
||||||
|
final int addrDogecoinAmount = addrCounter++;
|
||||||
|
final int addrTradeTimeout = addrCounter++;
|
||||||
|
|
||||||
|
final int addrMessageTxnType = addrCounter++;
|
||||||
|
final int addrExpectedTradeMessageLength = addrCounter++;
|
||||||
|
final int addrExpectedRedeemMessageLength = addrCounter++;
|
||||||
|
|
||||||
|
final int addrCreatorAddressPointer = addrCounter++;
|
||||||
|
final int addrQortalPartnerAddressPointer = addrCounter++;
|
||||||
|
final int addrMessageSenderPointer = addrCounter++;
|
||||||
|
|
||||||
|
final int addrTradeMessagePartnerDogecoinPKHOffset = addrCounter++;
|
||||||
|
final int addrPartnerDogecoinPKHPointer = addrCounter++;
|
||||||
|
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
|
||||||
|
final int addrHashOfSecretAPointer = addrCounter++;
|
||||||
|
|
||||||
|
final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
|
||||||
|
|
||||||
|
final int addrMessageDataPointer = addrCounter++;
|
||||||
|
final int addrMessageDataLength = addrCounter++;
|
||||||
|
|
||||||
|
final int addrPartnerReceivingAddressPointer = addrCounter++;
|
||||||
|
|
||||||
|
final int addrEndOfConstants = addrCounter;
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
final int addrCreatorAddress1 = addrCounter++;
|
||||||
|
final int addrCreatorAddress2 = addrCounter++;
|
||||||
|
final int addrCreatorAddress3 = addrCounter++;
|
||||||
|
final int addrCreatorAddress4 = addrCounter++;
|
||||||
|
|
||||||
|
final int addrQortalPartnerAddress1 = addrCounter++;
|
||||||
|
final int addrQortalPartnerAddress2 = addrCounter++;
|
||||||
|
final int addrQortalPartnerAddress3 = addrCounter++;
|
||||||
|
final int addrQortalPartnerAddress4 = addrCounter++;
|
||||||
|
|
||||||
|
final int addrLockTimeA = addrCounter++;
|
||||||
|
final int addrRefundTimeout = addrCounter++;
|
||||||
|
final int addrRefundTimestamp = addrCounter++;
|
||||||
|
final int addrLastTxnTimestamp = addrCounter++;
|
||||||
|
final int addrBlockTimestamp = addrCounter++;
|
||||||
|
final int addrTxnType = addrCounter++;
|
||||||
|
final int addrResult = addrCounter++;
|
||||||
|
|
||||||
|
final int addrMessageSender1 = addrCounter++;
|
||||||
|
final int addrMessageSender2 = addrCounter++;
|
||||||
|
final int addrMessageSender3 = addrCounter++;
|
||||||
|
final int addrMessageSender4 = addrCounter++;
|
||||||
|
|
||||||
|
final int addrMessageLength = addrCounter++;
|
||||||
|
|
||||||
|
final int addrMessageData = addrCounter;
|
||||||
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrHashOfSecretA = addrCounter;
|
||||||
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrPartnerDogecoinPKH = addrCounter;
|
||||||
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrPartnerReceivingAddress = addrCounter;
|
||||||
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrMode = addrCounter++;
|
||||||
|
assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
|
||||||
|
|
||||||
|
// Data segment
|
||||||
|
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||||
|
|
||||||
|
// AT creator's trade Qortal address, decoded from Base58
|
||||||
|
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
|
||||||
|
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
|
||||||
|
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
|
||||||
|
|
||||||
|
// Dogecoin public key hash
|
||||||
|
assert dataByteBuffer.position() == addrDogecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrDogecoinPublicKeyHash incorrect";
|
||||||
|
dataByteBuffer.put(Bytes.ensureCapacity(dogecoinPublicKeyHash, 32, 0));
|
||||||
|
|
||||||
|
// Redeem Qort amount
|
||||||
|
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||||
|
dataByteBuffer.putLong(qortAmount);
|
||||||
|
|
||||||
|
// Expected Dogecoin amount
|
||||||
|
assert dataByteBuffer.position() == addrDogecoinAmount * MachineState.VALUE_SIZE : "addrDogecoinAmount incorrect";
|
||||||
|
dataByteBuffer.putLong(dogecoinAmount);
|
||||||
|
|
||||||
|
// Suggested trade timeout (minutes)
|
||||||
|
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
||||||
|
dataByteBuffer.putLong(tradeTimeout);
|
||||||
|
|
||||||
|
// We're only interested in MESSAGE transactions
|
||||||
|
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
|
||||||
|
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
|
||||||
|
|
||||||
|
// Expected length of 'trade' MESSAGE data from AT creator
|
||||||
|
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
|
||||||
|
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
|
||||||
|
|
||||||
|
// Expected length of 'redeem' MESSAGE data from trade partner
|
||||||
|
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
|
||||||
|
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
|
||||||
|
|
||||||
|
// Index into data segment of AT creator's address, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrCreatorAddress1);
|
||||||
|
|
||||||
|
// Index into data segment of partner's Qortal address, used by SET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrQortalPartnerAddress1);
|
||||||
|
|
||||||
|
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrMessageSender1);
|
||||||
|
|
||||||
|
// Offset into 'trade' MESSAGE data payload for extracting partner's Dogecoin PKH
|
||||||
|
assert dataByteBuffer.position() == addrTradeMessagePartnerDogecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDogecoinPKHOffset incorrect";
|
||||||
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
|
// Index into data segment of partner's Dogecoin PKH, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrPartnerDogecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerDogecoinPKHPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrPartnerDogecoinPKH);
|
||||||
|
|
||||||
|
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||||
|
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
||||||
|
dataByteBuffer.putLong(64L);
|
||||||
|
|
||||||
|
// Index into data segment to hash of secret A, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrHashOfSecretA);
|
||||||
|
|
||||||
|
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
|
||||||
|
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
|
||||||
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
|
// Source location and length for hashing any passed secret
|
||||||
|
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrMessageData);
|
||||||
|
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
|
||||||
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
|
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrPartnerReceivingAddress);
|
||||||
|
|
||||||
|
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
|
||||||
|
|
||||||
|
// Code labels
|
||||||
|
Integer labelRefund = null;
|
||||||
|
|
||||||
|
Integer labelTradeTxnLoop = null;
|
||||||
|
Integer labelCheckTradeTxn = null;
|
||||||
|
Integer labelCheckCancelTxn = null;
|
||||||
|
Integer labelNotTradeNorCancelTxn = null;
|
||||||
|
Integer labelCheckNonRefundTradeTxn = null;
|
||||||
|
Integer labelTradeTxnExtract = null;
|
||||||
|
Integer labelRedeemTxnLoop = null;
|
||||||
|
Integer labelCheckRedeemTxn = null;
|
||||||
|
Integer labelCheckRedeemTxnSender = null;
|
||||||
|
Integer labelPayout = null;
|
||||||
|
|
||||||
|
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
|
||||||
|
|
||||||
|
// Two-pass version
|
||||||
|
for (int pass = 0; pass < 2; ++pass) {
|
||||||
|
codeByteBuffer.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
/* Initialization */
|
||||||
|
|
||||||
|
/* NOP - to ensure DOGECOIN ACCT is unique */
|
||||||
|
codeByteBuffer.put(OpCode.NOP.compile());
|
||||||
|
|
||||||
|
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
|
||||||
|
|
||||||
|
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
|
||||||
|
|
||||||
|
// Set restart position to after this opcode
|
||||||
|
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||||
|
|
||||||
|
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
|
||||||
|
|
||||||
|
/* Transaction processing loop */
|
||||||
|
labelTradeTxnLoop = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
|
||||||
|
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||||
|
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||||
|
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
|
||||||
|
// Stop and wait for next block
|
||||||
|
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||||
|
|
||||||
|
/* Check transaction */
|
||||||
|
labelCheckTradeTxn = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
|
||||||
|
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
|
||||||
|
// If transaction type is not MESSAGE type then go look for another transaction
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
|
||||||
|
|
||||||
|
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
|
||||||
|
|
||||||
|
// 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 message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||||
|
// Message sender's address matches AT creator's trade address so go process 'trade' message
|
||||||
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
|
||||||
|
|
||||||
|
/* Checking message sender for possible cancel message */
|
||||||
|
labelCheckCancelTxn = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||||
|
// Partner address is AT creator's address, so cancel offer and finish.
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
|
||||||
|
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||||
|
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||||
|
|
||||||
|
/* Not trade nor cancel message */
|
||||||
|
labelNotTradeNorCancelTxn = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Loop to find another transaction
|
||||||
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
|
||||||
|
|
||||||
|
/* Possible switch-to-trade-mode message */
|
||||||
|
labelCheckNonRefundTradeTxn = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Check 'trade' message we received has expected number of message bytes
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
|
||||||
|
// If message length matches, branch to info extraction code
|
||||||
|
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
|
||||||
|
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
|
||||||
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
|
||||||
|
|
||||||
|
/* Extracting info from 'trade' MESSAGE transaction */
|
||||||
|
labelTradeTxnExtract = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// 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 addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
|
||||||
|
|
||||||
|
// Extract trade partner's Dogecoin public key hash (PKH) from message into B
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDogecoinPKHOffset));
|
||||||
|
// Store partner's Dogecoin PKH (we only really use values from B1-B3)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDogecoinPKHPointer));
|
||||||
|
// Extract AT trade timeout (minutes) (from B4)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
|
||||||
|
|
||||||
|
// Grab next 32 bytes
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
|
||||||
|
|
||||||
|
// Extract hash-of-secret-A (we only really use values from B1-B3)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
|
||||||
|
// Extract lockTime-A (from B4)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
|
||||||
|
|
||||||
|
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
|
||||||
|
|
||||||
|
/* We are in 'trade mode' */
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
|
||||||
|
|
||||||
|
// Set restart position to after this opcode
|
||||||
|
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||||
|
|
||||||
|
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
|
||||||
|
|
||||||
|
// Fetch current block 'timestamp'
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
|
||||||
|
// If we're not past refund 'timestamp' then look for next transaction
|
||||||
|
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||||
|
// We're past refund 'timestamp' so go refund everything back to AT creator
|
||||||
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
|
||||||
|
|
||||||
|
/* Transaction processing loop */
|
||||||
|
labelRedeemTxnLoop = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Find next transaction to this AT since the last one (if any)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
|
||||||
|
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||||
|
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||||
|
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
|
||||||
|
// Stop and wait for next block
|
||||||
|
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||||
|
|
||||||
|
/* Check transaction */
|
||||||
|
labelCheckRedeemTxn = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
|
||||||
|
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
|
||||||
|
// If transaction type is not MESSAGE type then go look for another transaction
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||||
|
|
||||||
|
/* Check message payload length */
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
|
||||||
|
// If message length matches, branch to sender checking code
|
||||||
|
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
|
||||||
|
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
|
||||||
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
|
||||||
|
|
||||||
|
/* Check transaction's sender */
|
||||||
|
labelCheckRedeemTxnSender = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Extract sender address from transaction into B register
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
||||||
|
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
|
||||||
|
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||||
|
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||||
|
|
||||||
|
/* Check 'secret-A' in transaction's message */
|
||||||
|
|
||||||
|
// Extract secret-A from first 32 bytes of message from transaction into B register
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
||||||
|
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
|
||||||
|
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
|
||||||
|
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
|
||||||
|
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
|
||||||
|
// If hashes don't match, addrResult will be zero so go find another transaction
|
||||||
|
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
|
||||||
|
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
|
||||||
|
|
||||||
|
/* Success! Pay arranged amount to receiving address */
|
||||||
|
labelPayout = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
|
||||||
|
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
|
||||||
|
// Pay AT's balance to receiving address
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
|
||||||
|
// Set redeemed mode
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
|
||||||
|
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||||
|
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||||
|
|
||||||
|
// Fall-through to refunding any remaining balance back to AT creator
|
||||||
|
|
||||||
|
/* Refund balance back to AT creator */
|
||||||
|
labelRefund = codeByteBuffer.position();
|
||||||
|
|
||||||
|
// Set refunded mode
|
||||||
|
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
|
||||||
|
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||||
|
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||||
|
} catch (CompilationException e) {
|
||||||
|
throw new IllegalStateException("Unable to compile DOGE-QORT ACCT?", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeByteBuffer.flip();
|
||||||
|
|
||||||
|
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||||
|
codeByteBuffer.get(codeBytes);
|
||||||
|
|
||||||
|
assert Arrays.equals(Crypto.digest(codeBytes), DogecoinACCTv1.CODE_BYTES_HASH)
|
||||||
|
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
|
||||||
|
|
||||||
|
final short ciyamAtVersion = 2;
|
||||||
|
final short numCallStackPages = 0;
|
||||||
|
final short numUserStackPages = 0;
|
||||||
|
final long minActivationAmount = 0L;
|
||||||
|
|
||||||
|
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||||
|
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||||
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||||
|
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||||
|
*/
|
||||||
|
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||||
|
byte[] addressBytes = new byte[25]; // for general use
|
||||||
|
String atAddress = atStateData.getATAddress();
|
||||||
|
|
||||||
|
CrossChainTradeData tradeData = new CrossChainTradeData();
|
||||||
|
|
||||||
|
tradeData.foreignBlockchain = SupportedBlockchain.DOGECOIN.name();
|
||||||
|
tradeData.acctName = NAME;
|
||||||
|
|
||||||
|
tradeData.qortalAtAddress = atAddress;
|
||||||
|
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||||
|
tradeData.creationTimestamp = creationTimestamp;
|
||||||
|
|
||||||
|
Account atAccount = new Account(repository, atAddress);
|
||||||
|
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
|
byte[] stateData = atStateData.getStateData();
|
||||||
|
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||||
|
dataByteBuffer.position(MachineState.HEADER_LENGTH);
|
||||||
|
|
||||||
|
/* Constants */
|
||||||
|
|
||||||
|
// Skip creator's trade address
|
||||||
|
dataByteBuffer.get(addressBytes);
|
||||||
|
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||||
|
|
||||||
|
// Creator's Dogecoin/foreign public key hash
|
||||||
|
tradeData.creatorForeignPKH = new byte[20];
|
||||||
|
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||||
|
|
||||||
|
// We don't use secret-B
|
||||||
|
tradeData.hashOfSecretB = null;
|
||||||
|
|
||||||
|
// Redeem payout
|
||||||
|
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
|
// Expected DOGE amount
|
||||||
|
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
|
// Trade timeout
|
||||||
|
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||||
|
|
||||||
|
// Skip MESSAGE transaction type
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip expected 'trade' message length
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip expected 'redeem' message length
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to creator's address
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to partner's Qortal trade address
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to message sender
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip 'trade' message data offset for partner's Dogecoin PKH
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to partner's Dogecoin PKH
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip 'trade' message data offset for hash-of-secret-A
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to hash-of-secret-A
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip 'redeem' message data offset for partner's Qortal receiving address
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to message data
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip message data length
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to partner's receiving address
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
/* End of constants / begin variables */
|
||||||
|
|
||||||
|
// Skip AT creator's address
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||||
|
|
||||||
|
// Partner's trade address (if present)
|
||||||
|
dataByteBuffer.get(addressBytes);
|
||||||
|
String qortalRecipient = Base58.encode(addressBytes);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||||
|
|
||||||
|
// Potential lockTimeA (if in trade mode)
|
||||||
|
int lockTimeA = (int) dataByteBuffer.getLong();
|
||||||
|
|
||||||
|
// AT refund timeout (probably only useful for debugging)
|
||||||
|
int refundTimeout = (int) dataByteBuffer.getLong();
|
||||||
|
|
||||||
|
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
|
||||||
|
long tradeRefundTimestamp = dataByteBuffer.getLong();
|
||||||
|
|
||||||
|
// Skip last transaction timestamp
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip block timestamp
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip transaction type
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip temporary result
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip temporary message sender
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||||
|
|
||||||
|
// Skip message length
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip temporary message data
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||||
|
|
||||||
|
// Potential hash160 of secret A
|
||||||
|
byte[] hashOfSecretA = new byte[20];
|
||||||
|
dataByteBuffer.get(hashOfSecretA);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
||||||
|
|
||||||
|
// Potential partner's Dogecoin PKH
|
||||||
|
byte[] partnerDogecoinPKH = new byte[20];
|
||||||
|
dataByteBuffer.get(partnerDogecoinPKH);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDogecoinPKH.length); // skip to 32 bytes
|
||||||
|
|
||||||
|
// Partner's receiving address (if present)
|
||||||
|
byte[] partnerReceivingAddress = new byte[25];
|
||||||
|
dataByteBuffer.get(partnerReceivingAddress);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
|
||||||
|
|
||||||
|
// Trade AT's 'mode'
|
||||||
|
long modeValue = dataByteBuffer.getLong();
|
||||||
|
AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
|
||||||
|
|
||||||
|
/* End of variables */
|
||||||
|
|
||||||
|
if (mode != null && mode != AcctMode.OFFERING) {
|
||||||
|
tradeData.mode = mode;
|
||||||
|
tradeData.refundTimeout = refundTimeout;
|
||||||
|
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||||
|
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||||
|
tradeData.hashOfSecretA = hashOfSecretA;
|
||||||
|
tradeData.partnerForeignPKH = partnerDogecoinPKH;
|
||||||
|
tradeData.lockTimeA = lockTimeA;
|
||||||
|
|
||||||
|
if (mode == AcctMode.REDEEMED)
|
||||||
|
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
|
||||||
|
} else {
|
||||||
|
tradeData.mode = AcctMode.OFFERING;
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeData.duplicateDeprecated();
|
||||||
|
|
||||||
|
return tradeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
|
||||||
|
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
|
||||||
|
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||||
|
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
|
||||||
|
public static OfferMessageData extractOfferMessageData(byte[] messageData) {
|
||||||
|
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
OfferMessageData offerMessageData = new OfferMessageData();
|
||||||
|
offerMessageData.partnerDogecoinPKH = Arrays.copyOfRange(messageData, 0, 20);
|
||||||
|
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
|
||||||
|
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
|
||||||
|
|
||||||
|
return offerMessageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
|
||||||
|
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||||
|
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
|
||||||
|
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
|
||||||
|
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||||
|
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
|
||||||
|
|
||||||
|
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
|
||||||
|
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
|
||||||
|
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
|
||||||
|
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
|
||||||
|
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
|
||||||
|
@Override
|
||||||
|
public byte[] buildCancelMessage(String creatorQortalAddress) {
|
||||||
|
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
|
||||||
|
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
||||||
|
|
||||||
|
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
|
||||||
|
public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
|
||||||
|
byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
|
||||||
|
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
|
||||||
|
|
||||||
|
System.arraycopy(secretA, 0, data, 0, secretA.length);
|
||||||
|
System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
|
||||||
|
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
|
||||||
|
// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
|
||||||
|
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
|
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
|
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||||
|
|
||||||
|
// We don't have partner's public key so we check every message to AT
|
||||||
|
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
|
||||||
|
if (messageTransactionsData == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Find 'redeem' message
|
||||||
|
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
|
||||||
|
// Check message payload type/encryption
|
||||||
|
if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check message payload size
|
||||||
|
byte[] messageData = messageTransactionData.getData();
|
||||||
|
if (messageData.length != REDEEM_MESSAGE_LENGTH)
|
||||||
|
// Wrong payload length
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check sender
|
||||||
|
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
|
||||||
|
// Wrong sender;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Extract secretA
|
||||||
|
byte[] secretA = new byte[32];
|
||||||
|
System.arraycopy(messageData, 0, secretA, 0, secretA.length);
|
||||||
|
|
||||||
|
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||||
|
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return secretA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -33,6 +33,7 @@ import org.qortal.crypto.TrustlessSSLSocketFactory;
|
|||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
|
/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
|
||||||
public class ElectrumX extends BitcoinyBlockchainProvider {
|
public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||||
@ -171,13 +172,41 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
Long returnedCount = (Long) countObj;
|
Long returnedCount = (Long) countObj;
|
||||||
String hex = (String) hexObj;
|
String hex = (String) hexObj;
|
||||||
|
|
||||||
byte[] raw = HashCode.fromString(hex).asBytes();
|
|
||||||
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
|
|
||||||
throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
|
|
||||||
|
|
||||||
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
|
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
|
||||||
for (int i = 0; i < returnedCount; ++i)
|
|
||||||
|
byte[] raw = HashCode.fromString(hex).asBytes();
|
||||||
|
|
||||||
|
// Most chains use a fixed length 80 byte header, so block headers can be split up by dividing the hex into
|
||||||
|
// 80-byte segments. However, some chains such as DOGE use variable length headers due to AuxPoW or other
|
||||||
|
// reasons. In these cases we can identify the start of each block header by the location of the block version
|
||||||
|
// numbers. Each block starts with a version number, and for DOGE this is easily identifiable (6422788) at the
|
||||||
|
// time of writing (Jul 2021). If we encounter a chain that is using more generic version numbers (e.g. 1)
|
||||||
|
// and can't be used to accurately identify block indexes, then there are sufficient checks to ensure an
|
||||||
|
// exception is thrown.
|
||||||
|
|
||||||
|
if (raw.length == returnedCount * BLOCK_HEADER_LENGTH) {
|
||||||
|
// Fixed-length header (BTC, LTC, etc)
|
||||||
|
for (int i = 0; i < returnedCount; ++i) {
|
||||||
rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
|
rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (raw.length > returnedCount * BLOCK_HEADER_LENGTH) {
|
||||||
|
// Assume AuxPoW variable length header (DOGE)
|
||||||
|
int referenceVersion = BitTwiddling.intFromLEBytes(raw, 0); // DOGE uses 6422788 at time of commit (Jul 2021)
|
||||||
|
for (int i = 0; i < raw.length - 4; ++i) {
|
||||||
|
// Locate the start of each block by its version number
|
||||||
|
if (BitTwiddling.intFromLEBytes(raw, i) == referenceVersion) {
|
||||||
|
rawBlockHeaders.add(Arrays.copyOfRange(raw, i, i + BLOCK_HEADER_LENGTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure that we found the correct number of block headers
|
||||||
|
if (rawBlockHeaders.size() != count) {
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected raw header contents in JSON from ElectrumX blockchain.block.headers RPC.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (raw.length != returnedCount * BLOCK_HEADER_LENGTH) {
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
|
||||||
|
}
|
||||||
|
|
||||||
return rawBlockHeaders;
|
return rawBlockHeaders;
|
||||||
}
|
}
|
||||||
@ -518,6 +547,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Failed to perform RPC - maybe lack of servers?
|
// Failed to perform RPC - maybe lack of servers?
|
||||||
|
LOGGER.info("Error: No connected Electrum servers when trying to make RPC call");
|
||||||
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
|
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,6 @@ public interface ForeignBlockchain {
|
|||||||
|
|
||||||
public boolean isValidWalletKey(String walletKey);
|
public boolean isValidWalletKey(String walletKey);
|
||||||
|
|
||||||
|
public long getMinimumOrderAmount();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,10 @@ public class Litecoin extends Bitcoiny {
|
|||||||
new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001),
|
new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001),
|
||||||
new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002),
|
new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002),
|
||||||
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||||
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022));
|
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
||||||
|
new Server("electrum-ltc-bysh.me", Server.ConnectionType.TCP, 50002),
|
||||||
|
new Server("electrum.jochen-hoenicke.de", Server.ConnectionType.TCP, 50005),
|
||||||
|
new Server("node.ispol.sk", Server.ConnectionType.TCP, 50004));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -810,7 +810,8 @@ public class LitecoinACCTv1 implements ACCT {
|
|||||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
@Override
|
||||||
|
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||||
|
|
||||||
|
@ -39,6 +39,20 @@ public enum SupportedBlockchain {
|
|||||||
public ACCT getLatestAcct() {
|
public ACCT getLatestAcct() {
|
||||||
return LitecoinACCTv1.getInstance();
|
return LitecoinACCTv1.getInstance();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
DOGECOIN(Arrays.asList(
|
||||||
|
Triple.valueOf(DogecoinACCTv1.NAME, DogecoinACCTv1.CODE_BYTES_HASH, DogecoinACCTv1::getInstance)
|
||||||
|
)) {
|
||||||
|
@Override
|
||||||
|
public ForeignBlockchain getInstance() {
|
||||||
|
return Dogecoin.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ACCT getLatestAcct() {
|
||||||
|
return DogecoinACCTv1.getInstance();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
package org.qortal.gui;
|
package org.qortal.gui;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.*;
|
||||||
import java.awt.Image;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.awt.Dimension;
|
|
||||||
import java.awt.Graphics;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
import javax.swing.JDialog;
|
import javax.swing.*;
|
||||||
import javax.swing.JPanel;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -19,46 +15,53 @@ public class SplashFrame {
|
|||||||
protected static final Logger LOGGER = LogManager.getLogger(SplashFrame.class);
|
protected static final Logger LOGGER = LogManager.getLogger(SplashFrame.class);
|
||||||
|
|
||||||
private static SplashFrame instance;
|
private static SplashFrame instance;
|
||||||
private JDialog splashDialog;
|
private JFrame splashDialog;
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
public static class SplashPanel extends JPanel {
|
public static class SplashPanel extends JPanel {
|
||||||
private BufferedImage image;
|
private BufferedImage image;
|
||||||
|
|
||||||
|
private String defaultSplash = "Qlogo_512.png";
|
||||||
|
|
||||||
public SplashPanel() {
|
public SplashPanel() {
|
||||||
image = Gui.loadImage("splash.png");
|
image = Gui.loadImage(defaultSplash);
|
||||||
this.setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
|
|
||||||
this.setLayout(new BorderLayout());
|
setOpaque(false);
|
||||||
|
setLayout(new GridBagLayout());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void paintComponent(Graphics g) {
|
protected void paintComponent(Graphics g) {
|
||||||
super.paintComponent(g);
|
super.paintComponent(g);
|
||||||
g.drawImage(image, 0, 0, null);
|
g.drawImage(image, 0, 0, getWidth(), getHeight(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
return new Dimension(500, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SplashFrame() {
|
private SplashFrame() {
|
||||||
this.splashDialog = new JDialog();
|
this.splashDialog = new JFrame();
|
||||||
|
|
||||||
List<Image> icons = new ArrayList<>();
|
List<Image> icons = new ArrayList<>();
|
||||||
icons.add(Gui.loadImage("icons/icon16.png"));
|
icons.add(Gui.loadImage("icons/icon16.png"));
|
||||||
icons.add(Gui.loadImage("icons/icon32.png"));
|
icons.add(Gui.loadImage("icons/qortal_ui_tray_synced.png"));
|
||||||
|
icons.add(Gui.loadImage("icons/qortal_ui_tray_syncing_time-alt.png"));
|
||||||
|
icons.add(Gui.loadImage("icons/qortal_ui_tray_minting.png"));
|
||||||
|
icons.add(Gui.loadImage("icons/qortal_ui_tray_syncing.png"));
|
||||||
icons.add(Gui.loadImage("icons/icon64.png"));
|
icons.add(Gui.loadImage("icons/icon64.png"));
|
||||||
icons.add(Gui.loadImage("icons/icon128.png"));
|
icons.add(Gui.loadImage("icons/Qlogo_128.png"));
|
||||||
this.splashDialog.setIconImages(icons);
|
this.splashDialog.setIconImages(icons);
|
||||||
|
|
||||||
this.splashDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
this.splashDialog.getContentPane().add(new SplashPanel());
|
||||||
this.splashDialog.setTitle("qortal");
|
this.splashDialog.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||||
this.splashDialog.setContentPane(new SplashPanel());
|
|
||||||
|
|
||||||
this.splashDialog.setUndecorated(true);
|
this.splashDialog.setUndecorated(true);
|
||||||
this.splashDialog.setModal(false);
|
|
||||||
this.splashDialog.pack();
|
this.splashDialog.pack();
|
||||||
this.splashDialog.setLocationRelativeTo(null);
|
this.splashDialog.setLocationRelativeTo(null);
|
||||||
this.splashDialog.toFront();
|
this.splashDialog.setBackground(new Color(0,0,0,0));
|
||||||
this.splashDialog.setVisible(true);
|
this.splashDialog.setVisible(true);
|
||||||
this.splashDialog.repaint();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SplashFrame getInstance() {
|
public static SplashFrame getInstance() {
|
||||||
|
@ -61,7 +61,7 @@ public class SysTray {
|
|||||||
this.popupMenu = createJPopupMenu();
|
this.popupMenu = createJPopupMenu();
|
||||||
|
|
||||||
// Build TrayIcon without AWT PopupMenu (which doesn't support Unicode)...
|
// Build TrayIcon without AWT PopupMenu (which doesn't support Unicode)...
|
||||||
this.trayIcon = new TrayIcon(Gui.loadImage("icons/icon32.png"), "qortal", null);
|
this.trayIcon = new TrayIcon(Gui.loadImage("icons/qortal_ui_tray_synced.png"), "qortal", null);
|
||||||
// ...and attach mouse listener instead so we can use JPopupMenu (which does support Unicode)
|
// ...and attach mouse listener instead so we can use JPopupMenu (which does support Unicode)
|
||||||
this.trayIcon.addMouseListener(new MouseAdapter() {
|
this.trayIcon.addMouseListener(new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
@ -289,6 +289,25 @@ public class SysTray {
|
|||||||
this.trayIcon.setToolTip(text);
|
this.trayIcon.setToolTip(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTrayIcon(int iconid) {
|
||||||
|
if (trayIcon != null) {
|
||||||
|
switch (iconid) {
|
||||||
|
case 1:
|
||||||
|
this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_syncing_time-alt.png"));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_minting.png"));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_syncing.png"));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_synced.png"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (trayIcon != null)
|
if (trayIcon != null)
|
||||||
SystemTray.getSystemTray().remove(this.trayIcon);
|
SystemTray.getSystemTray().remove(this.trayIcon);
|
||||||
|
@ -72,7 +72,8 @@ public class Network {
|
|||||||
private static final String[] INITIAL_PEERS = new String[]{
|
private static final String[] INITIAL_PEERS = new String[]{
|
||||||
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
||||||
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org",
|
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org",
|
||||||
"node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk"
|
"node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk", "node22.qortal.org",
|
||||||
|
"cinfu1.crowetic.com", "node.cwd.systems"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
|
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
|
||||||
@ -80,6 +81,8 @@ public class Network {
|
|||||||
public static final int MAX_SIGNATURES_PER_REPLY = 500;
|
public static final int MAX_SIGNATURES_PER_REPLY = 500;
|
||||||
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
|
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
|
||||||
|
|
||||||
|
private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
|
||||||
|
|
||||||
// Generate our node keys / ID
|
// Generate our node keys / ID
|
||||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
|
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
|
||||||
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
|
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
|
||||||
@ -89,6 +92,8 @@ public class Network {
|
|||||||
private final int minOutboundPeers;
|
private final int minOutboundPeers;
|
||||||
private final int maxPeers;
|
private final int maxPeers;
|
||||||
|
|
||||||
|
private long nextDisconnectionCheck = 0L;
|
||||||
|
|
||||||
private final List<PeerData> allKnownPeers = new ArrayList<>();
|
private final List<PeerData> allKnownPeers = new ArrayList<>();
|
||||||
private final List<Peer> connectedPeers = new ArrayList<>();
|
private final List<Peer> connectedPeers = new ArrayList<>();
|
||||||
private final List<PeerAddress> selfPeers = new ArrayList<>();
|
private final List<PeerAddress> selfPeers = new ArrayList<>();
|
||||||
@ -611,6 +616,8 @@ public class Network {
|
|||||||
// Don't consider already connected peers (resolved address match)
|
// Don't consider already connected peers (resolved address match)
|
||||||
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
||||||
peers.removeIf(isResolvedAsConnectedPeer);
|
peers.removeIf(isResolvedAsConnectedPeer);
|
||||||
|
|
||||||
|
this.checkLongestConnection(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any left?
|
// Any left?
|
||||||
@ -668,6 +675,29 @@ public class Network {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkLongestConnection(Long now) {
|
||||||
|
if (now == null || now < nextDisconnectionCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find peers that have reached their maximum connection age, and disconnect them
|
||||||
|
List<Peer> peersToDisconnect = this.connectedPeers.stream()
|
||||||
|
.filter(peer -> !peer.isSyncInProgress())
|
||||||
|
.filter(peer -> peer.hasReachedMaxConnectionAge())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (peersToDisconnect != null && peersToDisconnect.size() > 0) {
|
||||||
|
for (Peer peer : peersToDisconnect) {
|
||||||
|
LOGGER.info("Forcing disconnection of peer {} because connection age ({} ms) " +
|
||||||
|
"has reached the maximum ({} ms)", peer, peer.getConnectionAge(), peer.getMaxConnectionAge());
|
||||||
|
peer.disconnect("Connection age too old");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check again after a minimum fixed interval
|
||||||
|
nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
// Peer callbacks
|
// Peer callbacks
|
||||||
|
|
||||||
protected void wakeupChannelSelector() {
|
protected void wakeupChannelSelector() {
|
||||||
|
@ -84,6 +84,7 @@ public class Peer {
|
|||||||
private Handshake handshakeStatus = Handshake.STARTED;
|
private Handshake handshakeStatus = Handshake.STARTED;
|
||||||
private volatile boolean handshakeMessagePending = false;
|
private volatile boolean handshakeMessagePending = false;
|
||||||
private long handshakeComplete = -1L;
|
private long handshakeComplete = -1L;
|
||||||
|
private long maxConnectionAge = 0L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of when socket was accepted, or connected.
|
* Timestamp of when socket was accepted, or connected.
|
||||||
@ -101,6 +102,8 @@ public class Peer {
|
|||||||
|
|
||||||
byte[] ourChallenge;
|
byte[] ourChallenge;
|
||||||
|
|
||||||
|
private boolean syncInProgress = false;
|
||||||
|
|
||||||
// Versioning
|
// Versioning
|
||||||
public static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX
|
public static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX
|
||||||
+ "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
|
+ "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
|
||||||
@ -197,10 +200,24 @@ public class Peer {
|
|||||||
this.handshakeStatus = handshakeStatus;
|
this.handshakeStatus = handshakeStatus;
|
||||||
if (handshakeStatus.equals(Handshake.COMPLETED)) {
|
if (handshakeStatus.equals(Handshake.COMPLETED)) {
|
||||||
this.handshakeComplete = System.currentTimeMillis();
|
this.handshakeComplete = System.currentTimeMillis();
|
||||||
|
this.generateRandomMaxConnectionAge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void generateRandomMaxConnectionAge() {
|
||||||
|
// Retrieve the min and max connection time from the settings, and calculate the range
|
||||||
|
final int minPeerConnectionTime = Settings.getInstance().getMinPeerConnectionTime();
|
||||||
|
final int maxPeerConnectionTime = Settings.getInstance().getMaxPeerConnectionTime();
|
||||||
|
final int peerConnectionTimeRange = maxPeerConnectionTime - minPeerConnectionTime;
|
||||||
|
|
||||||
|
// Generate a random number between the min and the max
|
||||||
|
Random random = new Random();
|
||||||
|
this.maxConnectionAge = (random.nextInt(peerConnectionTimeRange) + minPeerConnectionTime) * 1000L;
|
||||||
|
LOGGER.debug(String.format("[%s] Generated max connection age for peer %s. Min: %ds, max: %ds, range: %ds, random max: %dms", this.peerConnectionId, this, minPeerConnectionTime, maxPeerConnectionTime, peerConnectionTimeRange, this.maxConnectionAge));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
protected void resetHandshakeMessagePending() {
|
protected void resetHandshakeMessagePending() {
|
||||||
this.handshakeMessagePending = false;
|
this.handshakeMessagePending = false;
|
||||||
}
|
}
|
||||||
@ -330,6 +347,14 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSyncInProgress() {
|
||||||
|
return this.syncInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSyncInProgress(boolean syncInProgress) {
|
||||||
|
this.syncInProgress = syncInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
// Easier, and nicer output, than peer.getRemoteSocketAddress()
|
// Easier, and nicer output, than peer.getRemoteSocketAddress()
|
||||||
@ -812,4 +837,12 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
return handshakeComplete;
|
return handshakeComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getMaxConnectionAge() {
|
||||||
|
return maxConnectionAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasReachedMaxConnectionAge() {
|
||||||
|
return this.getConnectionAge() > this.getMaxConnectionAge();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ public class HSQLDBRepository implements Repository {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
|
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
|
||||||
|
|
||||||
private static final Object CHECKPOINT_LOCK = new Object();
|
public static final Object CHECKPOINT_LOCK = new Object();
|
||||||
|
|
||||||
// "serialization failure"
|
// "serialization failure"
|
||||||
private static final Integer DEADLOCK_ERROR_CODE = Integer.valueOf(-4861);
|
private static final Integer DEADLOCK_ERROR_CODE = Integer.valueOf(-4861);
|
||||||
@ -703,8 +703,11 @@ public class HSQLDBRepository implements Repository {
|
|||||||
private ResultSet checkedExecuteResultSet(PreparedStatement preparedStatement, Object... objects) throws SQLException {
|
private ResultSet checkedExecuteResultSet(PreparedStatement preparedStatement, Object... objects) throws SQLException {
|
||||||
bindStatementParams(preparedStatement, objects);
|
bindStatementParams(preparedStatement, objects);
|
||||||
|
|
||||||
|
// synchronize to block new executions if checkpointing in progress
|
||||||
|
synchronized (CHECKPOINT_LOCK) {
|
||||||
if (!preparedStatement.execute())
|
if (!preparedStatement.execute())
|
||||||
throw new SQLException("Fetching from database produced no results");
|
throw new SQLException("Fetching from database produced no results");
|
||||||
|
}
|
||||||
|
|
||||||
ResultSet resultSet = preparedStatement.getResultSet();
|
ResultSet resultSet = preparedStatement.getResultSet();
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
|
@ -61,6 +61,7 @@ public class HSQLDBSaver {
|
|||||||
public boolean execute(HSQLDBRepository repository) throws SQLException {
|
public boolean execute(HSQLDBRepository repository) throws SQLException {
|
||||||
String sql = this.formatInsertWithPlaceholders();
|
String sql = this.formatInsertWithPlaceholders();
|
||||||
|
|
||||||
|
synchronized (HSQLDBRepository.CHECKPOINT_LOCK) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement preparedStatement = repository.prepareStatement(sql);
|
PreparedStatement preparedStatement = repository.prepareStatement(sql);
|
||||||
this.bindValues(preparedStatement);
|
this.bindValues(preparedStatement);
|
||||||
@ -70,6 +71,7 @@ public class HSQLDBSaver {
|
|||||||
throw repository.examineException(e);
|
throw repository.examineException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format table and column names into an INSERT INTO ... SQL statement.
|
* Format table and column names into an INSERT INTO ... SQL statement.
|
||||||
|
@ -26,6 +26,7 @@ import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
|||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.crosschain.Bitcoin.BitcoinNet;
|
import org.qortal.crosschain.Bitcoin.BitcoinNet;
|
||||||
import org.qortal.crosschain.Litecoin.LitecoinNet;
|
import org.qortal.crosschain.Litecoin.LitecoinNet;
|
||||||
|
import org.qortal.crosschain.Dogecoin.DogecoinNet;
|
||||||
|
|
||||||
// All properties to be converted to JSON via JAXB
|
// All properties to be converted to JSON via JAXB
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@ -144,6 +145,11 @@ public class Settings {
|
|||||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||||
private boolean allowConnectionsWithOlderPeerVersions = true;
|
private boolean allowConnectionsWithOlderPeerVersions = true;
|
||||||
|
|
||||||
|
/** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||||
|
private int minPeerConnectionTime = 2 * 60; // seconds
|
||||||
|
/** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||||
|
private int maxPeerConnectionTime = 20 * 60; // seconds
|
||||||
|
|
||||||
/** Whether to sync multiple blocks at once in normal operation */
|
/** Whether to sync multiple blocks at once in normal operation */
|
||||||
private boolean fastSyncEnabled = true;
|
private boolean fastSyncEnabled = true;
|
||||||
/** Whether to sync multiple blocks at once when the peer has a different chain */
|
/** Whether to sync multiple blocks at once when the peer has a different chain */
|
||||||
@ -159,6 +165,7 @@ public class Settings {
|
|||||||
private String blockchainConfig = null; // use default from resources
|
private String blockchainConfig = null; // use default from resources
|
||||||
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
||||||
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
|
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
|
||||||
|
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
|
||||||
// Also crosschain-related:
|
// Also crosschain-related:
|
||||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||||
private boolean tradebotSystrayEnabled = false;
|
private boolean tradebotSystrayEnabled = false;
|
||||||
@ -507,6 +514,10 @@ public class Settings {
|
|||||||
|
|
||||||
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
|
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
|
||||||
|
|
||||||
|
public int getMinPeerConnectionTime() { return this.minPeerConnectionTime; }
|
||||||
|
|
||||||
|
public int getMaxPeerConnectionTime() { return this.maxPeerConnectionTime; }
|
||||||
|
|
||||||
public String getBlockchainConfig() {
|
public String getBlockchainConfig() {
|
||||||
return this.blockchainConfig;
|
return this.blockchainConfig;
|
||||||
}
|
}
|
||||||
@ -519,6 +530,10 @@ public class Settings {
|
|||||||
return this.litecoinNet;
|
return this.litecoinNet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DogecoinNet getDogecoinNet() {
|
||||||
|
return this.dogecoinNet;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTradebotSystrayEnabled() {
|
public boolean isTradebotSystrayEnabled() {
|
||||||
return this.tradebotSystrayEnabled;
|
return this.tradebotSystrayEnabled;
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
package org.qortal.utils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.qortal.globalization.BIP39WordList;
|
|
||||||
|
|
||||||
public class BIP39 {
|
|
||||||
|
|
||||||
private static final int BITS_PER_WORD = 11;
|
|
||||||
|
|
||||||
/** Convert BIP39 mnemonic to binary 'entropy' */
|
|
||||||
public static byte[] decode(String[] phraseWords, String lang) {
|
|
||||||
if (lang == null)
|
|
||||||
lang = "en";
|
|
||||||
|
|
||||||
List<String> wordList = BIP39WordList.INSTANCE.getByLang(lang);
|
|
||||||
if (wordList == null)
|
|
||||||
throw new IllegalStateException("BIP39 word list for lang '" + lang + "' unavailable");
|
|
||||||
|
|
||||||
byte[] entropy = new byte[(phraseWords.length * BITS_PER_WORD + 7) / 8];
|
|
||||||
int byteIndex = 0;
|
|
||||||
int bitShift = 3;
|
|
||||||
|
|
||||||
for (int i = 0; i < phraseWords.length; ++i) {
|
|
||||||
int wordListIndex = wordList.indexOf(phraseWords[i]);
|
|
||||||
if (wordListIndex == -1)
|
|
||||||
// Word not found
|
|
||||||
return null;
|
|
||||||
|
|
||||||
entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift);
|
|
||||||
|
|
||||||
bitShift = 8 - bitShift;
|
|
||||||
if (bitShift >= 0) {
|
|
||||||
// Leftover fits inside one byte
|
|
||||||
entropy[byteIndex] |= (byte) ((wordListIndex << bitShift));
|
|
||||||
bitShift = BITS_PER_WORD - bitShift;
|
|
||||||
} else {
|
|
||||||
// Leftover spread over next two bytes
|
|
||||||
bitShift = - bitShift;
|
|
||||||
entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift);
|
|
||||||
|
|
||||||
entropy[byteIndex] |= (byte) (wordListIndex << (8 - bitShift));
|
|
||||||
bitShift = bitShift + BITS_PER_WORD - 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entropy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert binary entropy to BIP39 mnemonic */
|
|
||||||
public static String encode(byte[] entropy, String lang) {
|
|
||||||
if (lang == null)
|
|
||||||
lang = "en";
|
|
||||||
|
|
||||||
List<String> wordList = BIP39WordList.INSTANCE.getByLang(lang);
|
|
||||||
if (wordList == null)
|
|
||||||
throw new IllegalStateException("BIP39 word list for lang '" + lang + "' unavailable");
|
|
||||||
|
|
||||||
List<String> phraseWords = new ArrayList<>();
|
|
||||||
|
|
||||||
int bitMask = 128; // MSB first
|
|
||||||
int byteIndex = 0;
|
|
||||||
while (true) {
|
|
||||||
int wordListIndex = 0;
|
|
||||||
for (int bitCount = 0; bitCount < BITS_PER_WORD; ++bitCount) {
|
|
||||||
wordListIndex <<= 1;
|
|
||||||
|
|
||||||
if ((entropy[byteIndex] & bitMask) != 0)
|
|
||||||
++wordListIndex;
|
|
||||||
|
|
||||||
bitMask >>= 1;
|
|
||||||
if (bitMask == 0) {
|
|
||||||
bitMask = 128;
|
|
||||||
++byteIndex;
|
|
||||||
|
|
||||||
if (byteIndex >= entropy.length)
|
|
||||||
return String.join(" ", phraseWords);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
phraseWords.add(wordList.get(wordListIndex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -65,6 +65,8 @@ TRANSFORMATION_ERROR = could not transform JSON into transaction
|
|||||||
|
|
||||||
UNAUTHORIZED = API call unauthorized
|
UNAUTHORIZED = API call unauthorized
|
||||||
|
|
||||||
|
ORDER_SIZE_TOO_SMALL = order size too small
|
||||||
|
|
||||||
FILE_NOT_FOUND = file not found
|
FILE_NOT_FOUND = file not found
|
||||||
|
|
||||||
NO_REPLY = peer didn't reply within the allowed time
|
NO_REPLY = peer didn't reply within the allowed time
|
||||||
|
66
src/main/resources/i18n/ApiError_nl.properties
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||||
|
# Keys are from api.ApiError enum
|
||||||
|
|
||||||
|
ADDRESS_UNKNOWN = account adres onbekend
|
||||||
|
|
||||||
|
BLOCKCHAIN_NEEDS_SYNC = blockchain dient eerst gesynchronizeerd te worden
|
||||||
|
|
||||||
|
# Blocks
|
||||||
|
BLOCK_UNKNOWN = blok onbekend
|
||||||
|
|
||||||
|
BTC_BALANCE_ISSUE = onvoldoende Bitcoin balans
|
||||||
|
|
||||||
|
BTC_NETWORK_ISSUE = Bitcoin/ElectrumX netwerk probleem
|
||||||
|
|
||||||
|
BTC_TOO_SOON = te vroeg om Bitcoin transactie te versturen (vergrendelingstijd/gemiddelde bloktijd)
|
||||||
|
|
||||||
|
CANNOT_MINT = account kan niet munten
|
||||||
|
|
||||||
|
GROUP_UNKNOWN = onbekende groep
|
||||||
|
|
||||||
|
INVALID_ADDRESS = ongeldig adres
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
INVALID_ASSET_ID = ongeldige asset ID
|
||||||
|
|
||||||
|
INVALID_CRITERIA = ongeldige zoekcriteria
|
||||||
|
|
||||||
|
INVALID_DATA = ongeldige gegevens
|
||||||
|
|
||||||
|
INVALID_HEIGHT = ongeldige blokhoogte
|
||||||
|
|
||||||
|
INVALID_NETWORK_ADDRESS = ongeldig netwerkadres
|
||||||
|
|
||||||
|
INVALID_ORDER_ID = ongeldige asset order ID
|
||||||
|
|
||||||
|
INVALID_PRIVATE_KEY = ongeldige private key
|
||||||
|
|
||||||
|
INVALID_PUBLIC_KEY = ongeldige public key
|
||||||
|
|
||||||
|
INVALID_REFERENCE = ongeldige verwijzing
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
INVALID_SIGNATURE = ongeldige handtekening
|
||||||
|
|
||||||
|
JSON = lezen van JSON bericht gefaald
|
||||||
|
|
||||||
|
NAME_UNKNOWN = onbekende naam
|
||||||
|
|
||||||
|
NON_PRODUCTION = deze API call is niet toegestaan voor productiesystemen
|
||||||
|
|
||||||
|
NO_TIME_SYNC = klok nog niet gesynchronizeerd
|
||||||
|
|
||||||
|
ORDER_UNKNOWN = onbekende asset order ID
|
||||||
|
|
||||||
|
PUBLIC_KEY_NOT_FOUND = public key niet gevonden
|
||||||
|
|
||||||
|
REPOSITORY_ISSUE = repository fout
|
||||||
|
|
||||||
|
# This one is special in that caller expected to pass two additional strings, hence the two %s
|
||||||
|
TRANSACTION_INVALID = ongeldige transactie: %s (%s)
|
||||||
|
|
||||||
|
TRANSACTION_UNKNOWN = onbekende transactie
|
||||||
|
|
||||||
|
TRANSFORMATION_ERROR = JSON kon niet omgezet worden in transactie
|
||||||
|
|
||||||
|
UNAUTHORIZED = ongeautoriseerde API call
|
45
src/main/resources/i18n/SysTray_nl.properties
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||||
|
# SysTray pop-up menu
|
||||||
|
|
||||||
|
APPLYING_UPDATE_AND_RESTARTING = Automatische update en herstart worden uitgevoerd...
|
||||||
|
|
||||||
|
AUTO_UPDATE = Automatische Update
|
||||||
|
|
||||||
|
BLOCK_HEIGHT = hoogte
|
||||||
|
|
||||||
|
CHECK_TIME_ACCURACY = Controleer accuraatheid van de tijd
|
||||||
|
|
||||||
|
CONNECTING = Verbinden
|
||||||
|
|
||||||
|
CONNECTION = verbinding
|
||||||
|
|
||||||
|
CONNECTIONS = verbindingen
|
||||||
|
|
||||||
|
CREATING_BACKUP_OF_DB_FILES = Backup van databasebestanden wordt gemaakt...
|
||||||
|
|
||||||
|
DB_BACKUP = Database Backup
|
||||||
|
|
||||||
|
DB_CHECKPOINT = Database Controlepunt
|
||||||
|
|
||||||
|
EXIT = Verlaten
|
||||||
|
|
||||||
|
MINTING_DISABLED = NIET muntend
|
||||||
|
|
||||||
|
MINTING_ENABLED = \u2714 Muntend
|
||||||
|
|
||||||
|
# Nagging about lack of NTP time sync
|
||||||
|
NTP_NAG_CAPTION = Klok van de computer is inaccuraat!
|
||||||
|
|
||||||
|
NTP_NAG_TEXT_UNIX = Installeer NTP service voor een accurate klok.
|
||||||
|
|
||||||
|
NTP_NAG_TEXT_WINDOWS = Selecteer "Synchronizeer klok" uit het menu om op te lossen.
|
||||||
|
|
||||||
|
OPEN_UI = Open UI
|
||||||
|
|
||||||
|
PERFORMING_DB_CHECKPOINT = Nieuwe veranderingen aan database worden opgeslagen...
|
||||||
|
|
||||||
|
SYNCHRONIZE_CLOCK = Synchronizeer klok
|
||||||
|
|
||||||
|
SYNCHRONIZING_BLOCKCHAIN = Aan het synchronizeren
|
||||||
|
|
||||||
|
SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd
|
@ -1,31 +1,31 @@
|
|||||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||||
# SysTray pop-up menu
|
# SysTray pop-up menu
|
||||||
|
|
||||||
BLOCK_HEIGHT = 块高度
|
BLOCK_HEIGHT = 区块高度
|
||||||
|
|
||||||
CHECK_TIME_ACCURACY = 检查时间准确性
|
CHECK_TIME_ACCURACY = 检查时间准确性
|
||||||
|
|
||||||
CONNECTION = 个连接
|
CONNECTION = 个链接
|
||||||
|
|
||||||
CONNECTIONS = 个连接
|
CONNECTIONS = 个链接
|
||||||
|
|
||||||
EXIT = 退出软件
|
EXIT = 退出核心
|
||||||
|
|
||||||
MINTING_DISABLED = 没有铸币
|
MINTING_DISABLED = 没有铸币
|
||||||
|
|
||||||
MINTING_ENABLED = ✔ 铸币
|
MINTING_ENABLED = ✔ 铸币
|
||||||
|
|
||||||
# Nagging about lack of NTP time sync
|
# Nagging about lack of NTP time sync
|
||||||
NTP_NAG_CAPTION = 电脑的时钟不准确!
|
NTP_NAG_CAPTION = 电脑的时间不准确!
|
||||||
|
|
||||||
NTP_NAG_TEXT_UNIX = 安装NTP服务以获得准确的时钟。
|
NTP_NAG_TEXT_UNIX = 安装NTP服务以获取准确的时间。
|
||||||
|
|
||||||
NTP_NAG_TEXT_WINDOWS = 从菜单中选择“同步时钟”进行修复。
|
NTP_NAG_TEXT_WINDOWS = 从菜单中选择“同步时钟”进行修复。
|
||||||
|
|
||||||
OPEN_UI = 开启界面
|
OPEN_UI = 开启Qortal界面
|
||||||
|
|
||||||
SYNCHRONIZE_CLOCK = 同步时钟
|
SYNCHRONIZE_CLOCK = 同步时钟
|
||||||
|
|
||||||
SYNCHRONIZING_BLOCKCHAIN = 同步区块链
|
SYNCHRONIZING_BLOCKCHAIN = 正在同步区块链
|
||||||
|
|
||||||
SYNCHRONIZING_CLOCK = 同步着时钟
|
SYNCHRONIZING_CLOCK = 正在同步时钟
|
31
src/main/resources/i18n/SysTray_zh_TC.properties
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||||
|
# SysTray pop-up menu
|
||||||
|
|
||||||
|
BLOCK_HEIGHT = 區塊高度
|
||||||
|
|
||||||
|
CHECK_TIME_ACCURACY = 檢查時間準確性
|
||||||
|
|
||||||
|
CONNECTION = 個鏈接
|
||||||
|
|
||||||
|
CONNECTIONS = 個鏈接
|
||||||
|
|
||||||
|
EXIT = 退出核心
|
||||||
|
|
||||||
|
MINTING_DISABLED = 沒有鑄幣
|
||||||
|
|
||||||
|
MINTING_ENABLED = ✔ 鑄幣
|
||||||
|
|
||||||
|
# Nagging about lack of NTP time sync
|
||||||
|
NTP_NAG_CAPTION = 電腦的時間不準確!
|
||||||
|
|
||||||
|
NTP_NAG_TEXT_UNIX = 安装NTP服務以獲取準確的時間。
|
||||||
|
|
||||||
|
NTP_NAG_TEXT_WINDOWS = 從菜單中選擇“同步時鐘”進行修復。
|
||||||
|
|
||||||
|
OPEN_UI = 開啓Qortal界面
|
||||||
|
|
||||||
|
SYNCHRONIZE_CLOCK = 同步時鐘
|
||||||
|
|
||||||
|
SYNCHRONIZING_BLOCKCHAIN = 正在同步區塊鏈
|
||||||
|
|
||||||
|
SYNCHRONIZING_CLOCK = 正在同步時鐘
|
184
src/main/resources/i18n/TransactionValidity_nl.properties
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
|
||||||
|
ACCOUNT_ALREADY_EXISTS = account bestaat al
|
||||||
|
|
||||||
|
ACCOUNT_CANNOT_REWARD_SHARE = account kan geen beloningen delen
|
||||||
|
|
||||||
|
ALREADY_GROUP_ADMIN = reeds groepsadministrator
|
||||||
|
|
||||||
|
ALREADY_GROUP_MEMBER = reeds groepslid
|
||||||
|
|
||||||
|
ALREADY_VOTED_FOR_THAT_OPTION = reeds gestemd voor die optie
|
||||||
|
|
||||||
|
ASSET_ALREADY_EXISTS = asset bestaat al
|
||||||
|
|
||||||
|
ASSET_DOES_NOT_EXIST = asset bestaat niet
|
||||||
|
|
||||||
|
ASSET_DOES_NOT_MATCH_AT = asset matcht niet met de asset van de AT
|
||||||
|
|
||||||
|
ASSET_NOT_SPENDABLE = asset is niet uitgeefbaar
|
||||||
|
|
||||||
|
AT_ALREADY_EXISTS = AT bestaat al
|
||||||
|
|
||||||
|
AT_IS_FINISHED = AT is afgelopen
|
||||||
|
|
||||||
|
AT_UNKNOWN = AT onbekend
|
||||||
|
|
||||||
|
BANNED_FROM_GROUP = verbannen uit groep
|
||||||
|
|
||||||
|
BAN_EXISTS = ban bestaat al
|
||||||
|
|
||||||
|
BAN_UNKNOWN = ban onbekend
|
||||||
|
|
||||||
|
BUYER_ALREADY_OWNER = koper is al eigenaar
|
||||||
|
|
||||||
|
CHAT = CHAT transacties zijn nooit geldig voor opname in blokken
|
||||||
|
|
||||||
|
CLOCK_NOT_SYNCED = klok is niet gesynchronizeerd
|
||||||
|
|
||||||
|
DUPLICATE_OPTION = dubbele optie
|
||||||
|
|
||||||
|
GROUP_ALREADY_EXISTS = groep bestaat reeds
|
||||||
|
|
||||||
|
GROUP_APPROVAL_DECIDED = groepsgoedkeuring reeds afgewezen
|
||||||
|
|
||||||
|
GROUP_APPROVAL_NOT_REQUIRED = groepsgoedkeuring niet vereist
|
||||||
|
|
||||||
|
GROUP_DOES_NOT_EXIST = groep bestaat niet
|
||||||
|
|
||||||
|
GROUP_ID_MISMATCH = ongeldige match met groep-ID
|
||||||
|
|
||||||
|
GROUP_OWNER_CANNOT_LEAVE = groepseigenaar kan de groep niet verlaten
|
||||||
|
|
||||||
|
HAVE_EQUALS_WANT = have-asset is gelijk aan want-asset
|
||||||
|
|
||||||
|
INCORRECT_NONCE = incorrecte PoW nonce
|
||||||
|
|
||||||
|
INSUFFICIENT_FEE = vergoeding te laag
|
||||||
|
|
||||||
|
INVALID_ADDRESS = ongeldig adres
|
||||||
|
|
||||||
|
INVALID_AMOUNT = ongeldige hoeveelheid
|
||||||
|
|
||||||
|
INVALID_ASSET_OWNER = ongeldige asset-eigenaar
|
||||||
|
|
||||||
|
INVALID_AT_TRANSACTION = ongeldige AT-transactie
|
||||||
|
|
||||||
|
INVALID_AT_TYPE_LENGTH = ongeldige lengte voor AT 'type'
|
||||||
|
|
||||||
|
INVALID_CREATION_BYTES = ongeldige creation bytes
|
||||||
|
|
||||||
|
INVALID_DATA_LENGTH = ongeldige lengte voor data
|
||||||
|
|
||||||
|
INVALID_DESCRIPTION_LENGTH = ongeldige lengte voor beschrijving
|
||||||
|
|
||||||
|
INVALID_GROUP_APPROVAL_THRESHOLD = ongeldige drempelwaarde voor groepsgoedkeuring
|
||||||
|
|
||||||
|
INVALID_GROUP_BLOCK_DELAY = ongeldige groepsgoedkeuring voor blokvertraging
|
||||||
|
|
||||||
|
INVALID_GROUP_ID = ongeldige groep-ID
|
||||||
|
|
||||||
|
INVALID_GROUP_OWNER = ongeldige groepseigenaar
|
||||||
|
|
||||||
|
INVALID_LIFETIME = ongeldige levensduur
|
||||||
|
|
||||||
|
INVALID_NAME_LENGTH = ongeldige lengte voor naam
|
||||||
|
|
||||||
|
INVALID_NAME_OWNER = ongeldige naam voor eigenaar
|
||||||
|
|
||||||
|
INVALID_OPTIONS_COUNT = ongeldige hoeveelheid opties
|
||||||
|
|
||||||
|
INVALID_OPTION_LENGTH = ongeldige lengte voor opties
|
||||||
|
|
||||||
|
INVALID_ORDER_CREATOR = ongeldige aanmaker voor order
|
||||||
|
|
||||||
|
INVALID_PAYMENTS_COUNT = ongeldige hoeveelheid betalingen
|
||||||
|
|
||||||
|
INVALID_PUBLIC_KEY = ongeldige public key
|
||||||
|
|
||||||
|
INVALID_QUANTITY = ongeldige hoeveelheid
|
||||||
|
|
||||||
|
INVALID_REFERENCE = ongeldige verwijzing
|
||||||
|
|
||||||
|
INVALID_RETURN = ongeldige return
|
||||||
|
|
||||||
|
INVALID_REWARD_SHARE_PERCENT = ongeldig percentage voor beloningsdeling
|
||||||
|
|
||||||
|
INVALID_SELLER = ongeldige verkoper
|
||||||
|
|
||||||
|
INVALID_TAGS_LENGTH = ongeldige lengte voor 'tags'
|
||||||
|
|
||||||
|
INVALID_TX_GROUP_ID = ongeldige transactiegroep-ID
|
||||||
|
|
||||||
|
INVALID_VALUE_LENGTH = ongeldige lengte voor 'waarde'
|
||||||
|
|
||||||
|
INVITE_UNKNOWN = onbekende groepsuitnodiging
|
||||||
|
|
||||||
|
JOIN_REQUEST_EXISTS = aanvraag om lid van groep te worden bestaat al
|
||||||
|
|
||||||
|
MAXIMUM_REWARD_SHARES = limiet aan beloningsdelingen voor dit account is bereikt
|
||||||
|
|
||||||
|
MISSING_CREATOR = ontbrekende aanmaker
|
||||||
|
|
||||||
|
MULTIPLE_NAMES_FORBIDDEN = het registreren van meerdere namen op een account is niet toegestaan
|
||||||
|
|
||||||
|
NAME_ALREADY_FOR_SALE = naam reeds te koop
|
||||||
|
|
||||||
|
NAME_ALREADY_REGISTERED = naam reeds geregistreerd
|
||||||
|
|
||||||
|
NAME_DOES_NOT_EXIST = naam bestaat niet
|
||||||
|
|
||||||
|
NAME_NOT_FOR_SALE = naam is niet te koop
|
||||||
|
|
||||||
|
NAME_NOT_NORMALIZED = naam is niet in 'genormalizeerde' Unicode-vorm
|
||||||
|
|
||||||
|
NEGATIVE_AMOUNT = ongeldige/negatieve hoeveelheid
|
||||||
|
|
||||||
|
NEGATIVE_FEE = ongeldige/negatieve vergoeding
|
||||||
|
|
||||||
|
NEGATIVE_PRICE = ongeldige/negatieve prijs
|
||||||
|
|
||||||
|
NOT_GROUP_ADMIN = account is geen groepsadministrator
|
||||||
|
|
||||||
|
NOT_GROUP_MEMBER = account is geen groepslid
|
||||||
|
|
||||||
|
NOT_MINTING_ACCOUNT = account kan niet munten
|
||||||
|
|
||||||
|
NOT_YET_RELEASED = functie nog niet uitgebracht
|
||||||
|
|
||||||
|
NO_BALANCE = onvoldoende balans
|
||||||
|
|
||||||
|
NO_BLOCKCHAIN_LOCK = blockchain van node is momenteel bezig
|
||||||
|
|
||||||
|
NO_FLAG_PERMISSION = account heeft hier geen toestemming voor
|
||||||
|
|
||||||
|
OK = Oke
|
||||||
|
|
||||||
|
ORDER_ALREADY_CLOSED = asset handelsorder is al gesloten
|
||||||
|
|
||||||
|
ORDER_DOES_NOT_EXIST = asset handelsorder bestaat niet
|
||||||
|
|
||||||
|
POLL_ALREADY_EXISTS = peiling bestaat al
|
||||||
|
|
||||||
|
POLL_DOES_NOT_EXIST = peiling bestaat niet
|
||||||
|
|
||||||
|
POLL_OPTION_DOES_NOT_EXIST = peilingsoptie bestaat niet
|
||||||
|
|
||||||
|
PUBLIC_KEY_UNKNOWN = public key onbekend
|
||||||
|
|
||||||
|
REWARD_SHARE_UNKNOWN = beloningsdeling onbekend
|
||||||
|
|
||||||
|
SELF_SHARE_EXISTS = zelfdeling (beloningsdeling) bestaat reeds
|
||||||
|
|
||||||
|
TIMESTAMP_TOO_NEW = tijdstempel te nieuw
|
||||||
|
|
||||||
|
TIMESTAMP_TOO_OLD = tijdstempel te oud
|
||||||
|
|
||||||
|
TOO_MANY_UNCONFIRMED = account heeft te veel onbevestigde transacties in afwachting
|
||||||
|
|
||||||
|
TRANSACTION_ALREADY_CONFIRMED = transactie is reeds bevestigd
|
||||||
|
|
||||||
|
TRANSACTION_ALREADY_EXISTS = transactie bestaat al
|
||||||
|
|
||||||
|
TRANSACTION_UNKNOWN = transactie onbekend
|
||||||
|
|
||||||
|
TX_GROUP_ID_MISMATCH = groep-ID van transactie matcht niet
|
BIN
src/main/resources/images/Qlogo_512.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
src/main/resources/images/icons/Qlogo_128.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 2.6 KiB |
BIN
src/main/resources/images/icons/qortal_ui_tray_minting.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/main/resources/images/icons/qortal_ui_tray_synced.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/main/resources/images/icons/qortal_ui_tray_syncing.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.5 KiB |
0
src/main/resources/images/splash.png
Executable file → Normal file
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
@ -261,11 +261,11 @@ public class RepositoryTests extends Common {
|
|||||||
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
|
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
|
||||||
@Test
|
@Test
|
||||||
public void testBlockHeightSpeed() throws DataException, SQLException {
|
public void testBlockHeightSpeed() throws DataException, SQLException {
|
||||||
final int mintBlockCount = 30000;
|
final int mintBlockCount = 10000;
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// Mint some blocks
|
// Mint some blocks
|
||||||
System.out.println(String.format("Minting %d test blocks - should take approx. 30 seconds...", mintBlockCount));
|
System.out.println(String.format("Minting %d test blocks - should take approx. 10 seconds...", mintBlockCount));
|
||||||
|
|
||||||
long beforeBigMint = System.currentTimeMillis();
|
long beforeBigMint = System.currentTimeMillis();
|
||||||
for (int i = 0; i < mintBlockCount; ++i)
|
for (int i = 0; i < mintBlockCount; ++i)
|
||||||
|
@ -10,7 +10,6 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|||||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.utils.BIP39;
|
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
@ -44,15 +43,13 @@ public class VanityGen {
|
|||||||
byte checksum = (byte) (hash[0] & 0xf0);
|
byte checksum = (byte) (hash[0] & 0xf0);
|
||||||
byte[] entropy132 = Bytes.concat(entropy, new byte[] { checksum });
|
byte[] entropy132 = Bytes.concat(entropy, new byte[] { checksum });
|
||||||
|
|
||||||
String mnemonic = BIP39.encode(entropy132, "en");
|
|
||||||
|
|
||||||
PrivateKeyAccount account = new PrivateKeyAccount(null, hash);
|
PrivateKeyAccount account = new PrivateKeyAccount(null, hash);
|
||||||
|
|
||||||
if (!account.getAddress().startsWith(prefix))
|
if (!account.getAddress().startsWith(prefix))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
System.out.println(String.format("Address: %s, public key: %s, private key: %s, mnemonic: %s",
|
System.out.println(String.format("Address: %s, public key: %s, private key: %s",
|
||||||
account.getAddress(), Base58.encode(account.getPublicKey()), Base58.encode(hash), mnemonic));
|
account.getAddress(), Base58.encode(account.getPublicKey()), Base58.encode(hash)));
|
||||||
System.out.flush();
|
System.out.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
114
src/test/java/org/qortal/test/crosschain/DogecoinTests.java
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package org.qortal.test.crosschain;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
import org.bitcoinj.store.BlockStoreException;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qortal.crosschain.BitcoinyHTLC;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.Dogecoin;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.test.common.Common;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class DogecoinTests extends Common {
|
||||||
|
|
||||||
|
private Dogecoin dogecoin;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings(); // TestNet3
|
||||||
|
dogecoin = Dogecoin.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterTest() {
|
||||||
|
Dogecoin.resetForTesting();
|
||||||
|
dogecoin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||||
|
long before = System.currentTimeMillis();
|
||||||
|
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||||
|
long afterFirst = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||||
|
long afterSecond = System.currentTimeMillis();
|
||||||
|
|
||||||
|
long firstPeriod = afterFirst - before;
|
||||||
|
long secondPeriod = afterSecond - afterFirst;
|
||||||
|
|
||||||
|
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
|
||||||
|
|
||||||
|
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
|
||||||
|
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore(value = "Doesn't work, to be fixed later")
|
||||||
|
public void testFindHtlcSecret() throws ForeignBlockchainException {
|
||||||
|
// This actually exists on TEST3 but can take a while to fetch
|
||||||
|
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||||
|
|
||||||
|
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||||
|
byte[] secret = BitcoinyHTLC.findHtlcSecret(dogecoin, p2shAddress);
|
||||||
|
|
||||||
|
assertNotNull("secret not found", secret);
|
||||||
|
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildSpend() {
|
||||||
|
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||||
|
|
||||||
|
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||||
|
long amount = 1000L;
|
||||||
|
|
||||||
|
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
|
||||||
|
assertNotNull("insufficient funds", transaction);
|
||||||
|
|
||||||
|
// Check spent key caching doesn't affect outcome
|
||||||
|
|
||||||
|
transaction = dogecoin.buildSpend(xprv58, recipient, amount);
|
||||||
|
assertNotNull("insufficient funds", transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetWalletBalance() {
|
||||||
|
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||||
|
|
||||||
|
Long balance = dogecoin.getWalletBalance(xprv58);
|
||||||
|
|
||||||
|
assertNotNull(balance);
|
||||||
|
|
||||||
|
System.out.println(dogecoin.format(balance));
|
||||||
|
|
||||||
|
// Check spent key caching doesn't affect outcome
|
||||||
|
|
||||||
|
Long repeatBalance = dogecoin.getWalletBalance(xprv58);
|
||||||
|
|
||||||
|
assertNotNull(repeatBalance);
|
||||||
|
|
||||||
|
System.out.println(dogecoin.format(repeatBalance));
|
||||||
|
|
||||||
|
assertEquals(balance, repeatBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||||
|
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||||
|
|
||||||
|
String address = dogecoin.getUnusedReceiveAddress(xprv58);
|
||||||
|
|
||||||
|
assertNotNull(address);
|
||||||
|
|
||||||
|
System.out.println(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -67,7 +67,7 @@ git_url=https://github.com/${git_url##*:}
|
|||||||
git_url=${git_url%%.git}
|
git_url=${git_url%%.git}
|
||||||
|
|
||||||
# Check for EXE
|
# Check for EXE
|
||||||
exe=${project}-${git_tag#v}.exe
|
exe=${project}.exe
|
||||||
exe_src="${WINDOWS_INSTALLER_DIR}/${exe}"
|
exe_src="${WINDOWS_INSTALLER_DIR}/${exe}"
|
||||||
if [ ! -r "${exe_src}" ]; then
|
if [ ! -r "${exe_src}" ]; then
|
||||||
echo "Cannot find EXE installer at ${exe_src}"
|
echo "Cannot find EXE installer at ${exe_src}"
|
||||||
@ -75,7 +75,7 @@ if [ ! -r "${exe_src}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for ZIP
|
# Check for ZIP
|
||||||
zip_filename=${project}-${git_tag#v}.zip
|
zip_filename=${project}.zip
|
||||||
zip_src=${saved_pwd}/${zip_filename}
|
zip_src=${saved_pwd}/${zip_filename}
|
||||||
if [ ! -r "${zip_src}" ]; then
|
if [ ! -r "${zip_src}" ]; then
|
||||||
echo "Cannot find ZIP at ${zip_src}"
|
echo "Cannot find ZIP at ${zip_src}"
|
||||||
|
@ -63,4 +63,4 @@ printf "{\n}\n" > ${build_dir}/settings.json
|
|||||||
gtouch -d ${commit_ts%%+??:??} ${build_dir} ${build_dir}/*
|
gtouch -d ${commit_ts%%+??:??} ${build_dir} ${build_dir}/*
|
||||||
|
|
||||||
rm -f ${saved_pwd}/${project}.zip
|
rm -f ${saved_pwd}/${project}.zip
|
||||||
(cd ${build_dir}/..; 7z a -r -tzip ${saved_pwd}/${project}-${git_tag#v}.zip ${project}/)
|
(cd ${build_dir}/..; 7z a -r -tzip ${saved_pwd}/${project}.zip ${project}/)
|
||||||
|