forked from Qortal/qortal
Merge branch 'trade-bot'
This commit is contained in:
commit
47679b7f6c
Binary file not shown.
@ -4,6 +4,6 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@ -3,10 +3,11 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.4</release>
|
||||
<release>1.3.5</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
</versions>
|
||||
<lastUpdated>20200414162728</lastUpdated>
|
||||
<lastUpdated>20200717104214</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
2
pom.xml
2
pom.xml
@ -9,7 +9,7 @@
|
||||
<bitcoinj.version>0.15.5</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.4</ciyam-at.version>
|
||||
<ciyam-at.version>1.3.5</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
|
@ -43,6 +43,8 @@ import org.qortal.api.websocket.ActiveChatsWebSocket;
|
||||
import org.qortal.api.websocket.AdminStatusWebSocket;
|
||||
import org.qortal.api.websocket.BlocksWebSocket;
|
||||
import org.qortal.api.websocket.ChatMessagesWebSocket;
|
||||
import org.qortal.api.websocket.TradeBotWebSocket;
|
||||
import org.qortal.api.websocket.TradeOffersWebSocket;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
@ -196,6 +198,8 @@ public class ApiService {
|
||||
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
||||
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
|
@ -25,6 +25,9 @@ public class CrossChainBitcoinRedeemRequest {
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
|
||||
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
|
||||
public byte[] receivingAccountInfo;
|
||||
|
||||
public CrossChainBitcoinRedeemRequest() {
|
||||
}
|
||||
|
||||
|
@ -12,20 +12,19 @@ public class CrossChainBuildRequest {
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long initialQortAmount;
|
||||
|
||||
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long finalQortAmount;
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fundingQortAmount;
|
||||
|
||||
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
|
||||
public byte[] bitcoinPublicKeyHash;
|
||||
|
||||
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
|
||||
public byte[] secretHash;
|
||||
public byte[] hashOfSecretB;
|
||||
|
||||
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
|
@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainCancelRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
@Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
@Schema(description = "Qortal trade AT address")
|
||||
public String atAddress;
|
||||
|
||||
public CrossChainCancelRequest() {
|
||||
|
@ -0,0 +1,86 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainOfferSummary {
|
||||
|
||||
// Properties
|
||||
|
||||
@Schema(description = "AT's Qortal address")
|
||||
public String qortalAtAddress;
|
||||
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
public String qortalCreator;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long btcAmount;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
private int tradeTimeout;
|
||||
|
||||
private BTCACCT.Mode mode;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
private String partnerQortalReceivingAddress;
|
||||
|
||||
protected CrossChainOfferSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
|
||||
this.qortalCreator = crossChainTradeData.qortalCreator;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
||||
this.tradeTimeout = crossChainTradeData.tradeTimeout;
|
||||
this.mode = crossChainTradeData.mode;
|
||||
this.timestamp = timestamp;
|
||||
this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
|
||||
}
|
||||
|
||||
public String getQortalAtAddress() {
|
||||
return this.qortalAtAddress;
|
||||
}
|
||||
|
||||
public String getQortalCreator() {
|
||||
return this.qortalCreator;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public long getBtcAmount() {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
public int getTradeTimeout() {
|
||||
return this.tradeTimeout;
|
||||
}
|
||||
|
||||
public BTCACCT.Mode getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public String getPartnerQortalReceivingAddress() {
|
||||
return this.partnerQortalReceivingAddress;
|
||||
}
|
||||
|
||||
}
|
@ -8,14 +8,20 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainSecretRequest {
|
||||
|
||||
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] recipientPublicKey;
|
||||
@Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] partnerPublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
|
||||
public byte[] secret;
|
||||
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
|
||||
public byte[] secretA;
|
||||
|
||||
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
|
||||
public byte[] secretB;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
public String receivingAddress;
|
||||
|
||||
public CrossChainSecretRequest() {
|
||||
}
|
||||
|
@ -8,14 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeRequest {
|
||||
|
||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] creatorPublicKey;
|
||||
@Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||
public byte[] tradePublicKey;
|
||||
|
||||
@Schema(description = "Qortal AT address")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Qortal address for trade partner/recipient")
|
||||
public String recipient;
|
||||
@Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
|
||||
public byte[] messageTransactionSignature;
|
||||
|
||||
public CrossChainTradeRequest() {
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeSummary {
|
||||
|
||||
private long tradeTimestamp;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long btcAmount;
|
||||
|
||||
protected CrossChainTradeSummary() {
|
||||
/* For JAXB */
|
||||
}
|
||||
|
||||
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
|
||||
this.tradeTimestamp = timestamp;
|
||||
this.qortAmount = crossChainTradeData.qortAmount;
|
||||
this.btcAmount = crossChainTradeData.expectedBitcoin;
|
||||
}
|
||||
|
||||
public long getTradeTimestamp() {
|
||||
return this.tradeTimestamp;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public long getBtcAmount() {
|
||||
return this.btcAmount;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotCreateRequest {
|
||||
|
||||
@Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
|
||||
public byte[] creatorPublicKey;
|
||||
|
||||
@Schema(description = "QORT amount paid out on successful trade", example = "80.40200000")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "81")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fundingQortAmount;
|
||||
|
||||
@Schema(description = "Bitcoin amount wanted in return", example = "0.00864200")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long bitcoinAmount;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
public int tradeTimeout;
|
||||
|
||||
@Schema(description = "Bitcoin address for receiving", example = "1NCTG9oLk41bU6pcehLNo9DVJup77EHAVx")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotCreateRequest() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotRespondRequest {
|
||||
|
||||
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
|
||||
public String atAddress;
|
||||
|
||||
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotRespondRequest() {
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.ChatNotifier;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -24,7 +23,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@ -33,7 +32,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, String> pathParams = this.getPathParams(session, "/{address}");
|
||||
Map<String, String> pathParams = getPathParams(session, "/{address}");
|
||||
|
||||
String address = pathParams.get("address");
|
||||
if (address == null || !Crypto.isValidAddress(address)) {
|
||||
@ -76,7 +75,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
this.marshall(stringWriter, activeChats);
|
||||
marshall(stringWriter, activeChats);
|
||||
|
||||
// Only output if something has changed
|
||||
String output = stringWriter.toString();
|
||||
|
@ -11,7 +11,6 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.controller.StatusNotifier;
|
||||
@ -21,7 +20,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class AdminStatusWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@ -57,7 +56,7 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
this.marshall(stringWriter, nodeStatus);
|
||||
marshall(stringWriter, nodeStatus);
|
||||
|
||||
// Only output if something has changed
|
||||
String output = stringWriter.toString();
|
||||
|
@ -3,7 +3,10 @@ package org.qortal.api.websocket;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
@ -13,24 +16,28 @@ import javax.xml.bind.Marshaller;
|
||||
import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.MarshallerProperties;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrorRoot;
|
||||
|
||||
interface ApiWebSocket {
|
||||
@SuppressWarnings("serial")
|
||||
abstract class ApiWebSocket extends WebSocketServlet {
|
||||
|
||||
default String getPathInfo(Session session) {
|
||||
private static final Map<Class<? extends ApiWebSocket>, List<Session>> SESSIONS_BY_CLASS = new HashMap<>();
|
||||
|
||||
protected static String getPathInfo(Session session) {
|
||||
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
|
||||
return upgradeRequest.getHttpServletRequest().getPathInfo();
|
||||
}
|
||||
|
||||
default Map<String, String> getPathParams(Session session, String pathSpec) {
|
||||
protected static Map<String, String> getPathParams(Session session, String pathSpec) {
|
||||
UriTemplatePathSpec uriTemplatePathSpec = new UriTemplatePathSpec(pathSpec);
|
||||
return uriTemplatePathSpec.getPathParams(this.getPathInfo(session));
|
||||
return uriTemplatePathSpec.getPathParams(getPathInfo(session));
|
||||
}
|
||||
|
||||
default void sendError(Session session, ApiError apiError) {
|
||||
protected static void sendError(Session session, ApiError apiError) {
|
||||
ApiErrorRoot apiErrorRoot = new ApiErrorRoot();
|
||||
apiErrorRoot.setApiError(apiError);
|
||||
|
||||
@ -43,7 +50,7 @@ interface ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
default void marshall(Writer writer, Object object) throws IOException {
|
||||
protected static void marshall(Writer writer, Object object) throws IOException {
|
||||
Marshaller marshaller = createMarshaller(object.getClass());
|
||||
|
||||
try {
|
||||
@ -53,7 +60,7 @@ interface ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
default void marshall(Writer writer, Collection<?> collection) throws IOException {
|
||||
protected static void marshall(Writer writer, Collection<?> collection) throws IOException {
|
||||
// If collection is empty then we're returning "[]" anyway
|
||||
if (collection.isEmpty()) {
|
||||
writer.append("[]");
|
||||
@ -92,4 +99,22 @@ interface ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public void onWebSocketConnect(Session session) {
|
||||
synchronized (SESSIONS_BY_CLASS) {
|
||||
SESSIONS_BY_CLASS.computeIfAbsent(this.getClass(), clazz -> new ArrayList<>()).add(session);
|
||||
}
|
||||
}
|
||||
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
synchronized (SESSIONS_BY_CLASS) {
|
||||
SESSIONS_BY_CLASS.get(this.getClass()).remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Session> getSessions() {
|
||||
synchronized (SESSIONS_BY_CLASS) {
|
||||
return new ArrayList<>(SESSIONS_BY_CLASS.get(this.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.model.BlockInfo;
|
||||
@ -23,7 +22,7 @@ import org.qortal.utils.Base58;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class BlocksWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@ -111,7 +110,7 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, blockInfo);
|
||||
marshall(stringWriter, blockInfo);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
|
@ -14,7 +14,6 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.ChatNotifier;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
@ -25,7 +24,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSocket {
|
||||
public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
@ -129,7 +128,7 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
try {
|
||||
this.marshall(stringWriter, chatMessages);
|
||||
marshall(stringWriter, chatMessages);
|
||||
|
||||
session.getRemote().sendStringByFuture(stringWriter.toString());
|
||||
} catch (IOException | WebSocketException e) {
|
||||
|
119
src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java
Normal file
119
src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java
Normal file
@ -0,0 +1,119 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.TradeBot;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
/** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */
|
||||
private static final Map<String, TradeBotData.State> PREVIOUS_STATES = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(TradeBotWebSocket.class);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
if (tradeBotEntries == null)
|
||||
// How do we properly fail here?
|
||||
return;
|
||||
|
||||
PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getState)));
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof TradeBot.StateChangeEvent))
|
||||
return;
|
||||
|
||||
TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData();
|
||||
String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey());
|
||||
|
||||
synchronized (PREVIOUS_STATES) {
|
||||
if (PREVIOUS_STATES.get(tradePrivateKey58) == tradeBotData.getState())
|
||||
// Not changed
|
||||
return;
|
||||
|
||||
PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getState());
|
||||
}
|
||||
|
||||
List<TradeBotData> tradeBotEntries = Collections.singletonList(tradeBotData);
|
||||
for (Session session : getSessions())
|
||||
sendEntries(session, tradeBotEntries);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
// Send all known trade-bot entries
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
if (tradeBotEntries == null) {
|
||||
session.close(4001, "repository issue fetching trade-bot entries");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendEntries(session, tradeBotEntries)) {
|
||||
session.close(4002, "websocket issue");
|
||||
return;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private boolean sendEntries(Session session, List<TradeBotData> tradeBotEntries) {
|
||||
try {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
marshall(stringWriter, tradeBotEntries);
|
||||
|
||||
String output = stringWriter.toString();
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (IOException e) {
|
||||
// No output this time?
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
212
src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
Normal file
212
src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
Normal file
@ -0,0 +1,212 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
|
||||
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.CrossChainOfferSummary;
|
||||
import org.qortal.controller.BlockNotifier;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class TradeOffersWebSocket extends ApiWebSocket {
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(TradeOffersWebSocket.class);
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
|
||||
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
||||
final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ATStateData> initialAtStates;
|
||||
|
||||
// We want ALL OFFERING trades
|
||||
Boolean isFinished = Boolean.FALSE;
|
||||
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
|
||||
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
|
||||
Integer minimumFinalHeight = null;
|
||||
|
||||
initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (initialAtStates == null) {
|
||||
session.close(4001, "repository issue fetching OFFERING trades");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save initial AT modes
|
||||
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
|
||||
|
||||
// Convert to offer summaries
|
||||
crossChainOfferSummaries = produceSummaries(repository, initialAtStates, null);
|
||||
|
||||
if (includeHistoric) {
|
||||
// We also want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
|
||||
long timestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
|
||||
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
|
||||
if (minimumFinalHeight != 0) {
|
||||
isFinished = Boolean.TRUE;
|
||||
dataByteOffset = null;
|
||||
expectedValue = null;
|
||||
++minimumFinalHeight; // because height is just *before* timestamp
|
||||
|
||||
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (historicAtStates == null) {
|
||||
session.close(4002, "repository issue fetching historic trades");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ATStateData historicAtState : historicAtStates) {
|
||||
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
|
||||
|
||||
switch (historicOfferSummary.getMode()) {
|
||||
case REDEEMED:
|
||||
case REFUNDED:
|
||||
case CANCELLED:
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add summary to initial burst
|
||||
crossChainOfferSummaries.add(historicOfferSummary);
|
||||
|
||||
// Save initial AT mode
|
||||
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
session.close(4003, "generic repository issue");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
|
||||
session.close(4004, "websocket issue");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockNotifier.Listener listener = blockData -> onNotify(session, blockData, previousAtModes);
|
||||
BlockNotifier.getInstance().register(session, listener);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
BlockNotifier.getInstance().deregister(session);
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private void onNotify(Session session, BlockData blockData, final Map<String, BTCACCT.Mode> previousAtModes) {
|
||||
List<CrossChainOfferSummary> crossChainOfferSummaries = null;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Find any new trade ATs since this block
|
||||
final Boolean isFinished = null;
|
||||
final Integer dataByteOffset = null;
|
||||
final Long expectedValue = null;
|
||||
final Integer minimumFinalHeight = blockData.getHeight();
|
||||
|
||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
|
||||
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
|
||||
null, null, null);
|
||||
|
||||
if (atStates == null)
|
||||
return;
|
||||
|
||||
crossChainOfferSummaries = produceSummaries(repository, atStates, blockData.getTimestamp());
|
||||
} catch (DataException e) {
|
||||
// No output this time
|
||||
}
|
||||
|
||||
synchronized (previousAtModes) { //NOSONAR squid:S2445 suppressed because previousAtModes is final and curried in lambda
|
||||
// Remove any entries unchanged from last time
|
||||
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
|
||||
|
||||
// Don't send anything if no results
|
||||
if (crossChainOfferSummaries.isEmpty())
|
||||
return;
|
||||
|
||||
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
|
||||
|
||||
if (!wasSent)
|
||||
return;
|
||||
|
||||
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
|
||||
try {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
marshall(stringWriter, crossChainOfferSummaries);
|
||||
|
||||
String output = stringWriter.toString();
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (IOException e) {
|
||||
// No output this time?
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
|
||||
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
|
||||
|
||||
long atStateTimestamp;
|
||||
|
||||
if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING)
|
||||
// We want when trade was created, not when it was last updated
|
||||
atStateTimestamp = atState.getCreation();
|
||||
else
|
||||
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||
|
||||
return new CrossChainOfferSummary(crossChainTradeData, atStateTimestamp);
|
||||
}
|
||||
|
||||
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, List<ATStateData> atStates, Long timestamp) throws DataException {
|
||||
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
|
||||
|
||||
for (ATStateData atState : atStates)
|
||||
offerSummaries.add(produceSummary(repository, atState, timestamp));
|
||||
|
||||
return offerSummaries;
|
||||
}
|
||||
|
||||
}
|
@ -17,7 +17,6 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.NullAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.CiyamAtSettings;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -30,13 +29,13 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.ATRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.AtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@ -133,9 +132,9 @@ public class QortalATAPI extends API {
|
||||
|
||||
byte[] signature = blockSummaries.get(0).getSignature();
|
||||
// Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature.
|
||||
this.setA2(state, fromBytes(signature, 52));
|
||||
this.setA3(state, fromBytes(signature, 60));
|
||||
this.setA4(state, fromBytes(signature, 68));
|
||||
this.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
|
||||
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
|
||||
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch previous block?", e);
|
||||
}
|
||||
@ -149,59 +148,27 @@ public class QortalATAPI extends API {
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
BlockRepository blockRepository = this.getRepository().getBlockRepository();
|
||||
|
||||
ATRepository.NextTransactionInfo nextTransactionInfo;
|
||||
try {
|
||||
int currentHeight = blockRepository.getBlockchainHeight();
|
||||
List<Transaction> blockTransactions = null;
|
||||
|
||||
while (height <= currentHeight) {
|
||||
if (blockTransactions == null) {
|
||||
BlockData blockData = blockRepository.fromHeight(height);
|
||||
|
||||
if (blockData == null)
|
||||
throw new DataException("Unable to fetch block " + height + " from repository?");
|
||||
|
||||
Block block = new Block(this.getRepository(), blockData);
|
||||
|
||||
blockTransactions = block.getTransactions();
|
||||
}
|
||||
|
||||
// No more transactions in this block? Try next block
|
||||
if (sequence >= blockTransactions.size()) {
|
||||
++height;
|
||||
sequence = 0;
|
||||
blockTransactions = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
Transaction transaction = blockTransactions.get(sequence);
|
||||
|
||||
// Transaction needs to be sent to specified recipient
|
||||
List<String> recipientAddresses = transaction.getRecipientAddresses();
|
||||
if (recipientAddresses.contains(atAddress)) {
|
||||
// Found a transaction
|
||||
|
||||
this.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
|
||||
|
||||
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
|
||||
byte[] signature = transaction.getTransactionData().getSignature();
|
||||
this.setA2(state, fromBytes(signature, 8));
|
||||
this.setA3(state, fromBytes(signature, 16));
|
||||
this.setA4(state, fromBytes(signature, 24));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction wasn't for us - keep going
|
||||
++sequence;
|
||||
}
|
||||
|
||||
// No more transactions - zero A and exit
|
||||
this.zeroA(state);
|
||||
nextTransactionInfo = this.getRepository().getATRepository().findNextTransaction(atAddress, height, sequence);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch next transaction?", e);
|
||||
}
|
||||
|
||||
if (nextTransactionInfo == null) {
|
||||
// No more transactions for AT at this time - zero A and exit
|
||||
this.zeroA(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Found a transaction
|
||||
|
||||
this.setA1(state, new Timestamp(nextTransactionInfo.height, timestamp.blockchainId, nextTransactionInfo.sequence).longValue());
|
||||
|
||||
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
|
||||
this.setA2(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 8));
|
||||
this.setA3(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 16));
|
||||
this.setA4(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 24));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -282,7 +249,7 @@ public class QortalATAPI extends API {
|
||||
|
||||
byte[] hash = Crypto.digest(input);
|
||||
|
||||
return fromBytes(hash, 0);
|
||||
return BitTwiddling.longFromBEBytes(hash, 0);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch latest block from repository?", e);
|
||||
}
|
||||
@ -296,30 +263,14 @@ public class QortalATAPI extends API {
|
||||
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = null;
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case MESSAGE:
|
||||
messageData = ((MessageTransactionData) transactionData).getData();
|
||||
break;
|
||||
|
||||
case AT:
|
||||
messageData = ((ATTransactionData) transactionData).getMessage();
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Check data length is appropriate, i.e. not larger than B
|
||||
if (messageData.length > 4 * 8)
|
||||
return;
|
||||
byte[] messageData = this.getMessageFromTransaction(transactionData);
|
||||
|
||||
// Pad messageData to fit B
|
||||
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
|
||||
if (messageData.length < 4 * 8)
|
||||
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
|
||||
|
||||
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
|
||||
this.setB(state, paddedMessageData);
|
||||
this.setB(state, messageData);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -457,12 +408,6 @@ public class QortalATAPI extends API {
|
||||
|
||||
// Utility methods
|
||||
|
||||
/** Convert part of little-endian byte[] to long */
|
||||
/* package */ static long fromBytes(byte[] bytes, int start) {
|
||||
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
|
||||
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
|
||||
}
|
||||
|
||||
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
|
||||
public static byte[] partialSignature(byte[] fullSignature) {
|
||||
return Arrays.copyOfRange(fullSignature, 8, 32);
|
||||
@ -473,7 +418,7 @@ public class QortalATAPI extends API {
|
||||
// Compare end of transaction's signature against A2 thru A4
|
||||
byte[] sig = transactionData.getSignature();
|
||||
|
||||
if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24))
|
||||
if (this.getA2(state) != BitTwiddling.longFromBEBytes(sig, 8) || this.getA3(state) != BitTwiddling.longFromBEBytes(sig, 16) || this.getA4(state) != BitTwiddling.longFromBEBytes(sig, 24))
|
||||
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
|
||||
}
|
||||
|
||||
@ -497,6 +442,20 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns message data from transaction. */
|
||||
/*package*/ byte[] getMessageFromTransaction(TransactionData transactionData) {
|
||||
switch (transactionData.getType()) {
|
||||
case MESSAGE:
|
||||
return ((MessageTransactionData) transactionData).getData();
|
||||
|
||||
case AT:
|
||||
return ((ATTransactionData) transactionData).getMessage();
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns AT's account */
|
||||
/* package */ Account getATAccount() {
|
||||
return new Account(this.repository, this.atData.getATAddress());
|
||||
@ -563,4 +522,8 @@ public class QortalATAPI extends API {
|
||||
super.setB(state, bBytes);
|
||||
}
|
||||
|
||||
protected void zeroB(MachineState state) {
|
||||
super.zeroB(state);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import org.ciyam.at.IllegalFunctionCodeException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
/**
|
||||
@ -22,8 +23,70 @@ import org.qortal.settings.Settings;
|
||||
*/
|
||||
public enum QortalFunctionCode {
|
||||
/**
|
||||
* <tt>0x0510</tt><br>
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
|
||||
* Returns length of message data from transaction in A.<br>
|
||||
* <tt>0x0501</tt><br>
|
||||
* If transaction has no 'message', returns -1.
|
||||
*/
|
||||
GET_MESSAGE_LENGTH_FROM_TX_IN_A(0x0501, 0, true) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
|
||||
TransactionData transactionData = api.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = api.getMessageFromTransaction(transactionData);
|
||||
|
||||
if (messageData == null)
|
||||
functionData.returnValue = -1L;
|
||||
else
|
||||
functionData.returnValue = (long) messageData.length;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Put offset 'message' from transaction in A into B<br>
|
||||
* <tt>0x0502 start-offset</tt><br>
|
||||
* Copies up to 32 bytes of message data, starting at <tt>start-offset</tt> into B.<br>
|
||||
* If transaction has no 'message', or <tt>start-offset</tt> out of bounds, then zero B<br>
|
||||
* Example 'message' could be 256-bit shared secret
|
||||
*/
|
||||
PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B(0x0502, 1, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
|
||||
// In case something goes wrong, or we don't have enough message data.
|
||||
api.zeroB(state);
|
||||
|
||||
if (functionData.value1 < 0 || functionData.value1 > Integer.MAX_VALUE)
|
||||
return;
|
||||
|
||||
int startOffset = functionData.value1.intValue();
|
||||
|
||||
TransactionData transactionData = api.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = api.getMessageFromTransaction(transactionData);
|
||||
|
||||
if (messageData == null || startOffset > messageData.length)
|
||||
return;
|
||||
|
||||
/*
|
||||
* Copy up to 32 bytes of message data into B,
|
||||
* retain order but pad with zeros in lower bytes.
|
||||
*
|
||||
* So a 4-byte message "a b c d" would copy thusly:
|
||||
* a b c d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
*/
|
||||
int byteCount = Math.min(32, messageData.length - startOffset);
|
||||
byte[] bBytes = new byte[32];
|
||||
|
||||
System.arraycopy(messageData, startOffset, bBytes, 0, byteCount);
|
||||
|
||||
api.setB(state, bBytes);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
|
||||
* <tt>0x0510</tt>
|
||||
*/
|
||||
CONVERT_B_TO_PKH(0x0510, 0, false) {
|
||||
@Override
|
||||
@ -38,8 +101,8 @@ public enum QortalFunctionCode {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0511</tt><br>
|
||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
|
||||
* <tt>0x0511</tt><br>
|
||||
* P2SH stored in lower 25 bytes of B.
|
||||
*/
|
||||
CONVERT_B_TO_P2SH(0x0511, 0, false) {
|
||||
@ -51,8 +114,8 @@ public enum QortalFunctionCode {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0512</tt><br>
|
||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
|
||||
* <tt>0x0512</tt><br>
|
||||
* Qortal address stored in lower 25 bytes of B.
|
||||
*/
|
||||
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
|
||||
|
@ -792,6 +792,9 @@ public class Controller extends Thread {
|
||||
this.notifyGroupMembershipChange = false;
|
||||
ChatNotifier.getInstance().onGroupMembershipChange();
|
||||
}
|
||||
|
||||
// Trade-bot might want to perform some actions too
|
||||
TradeBot.getInstance().onChainTipChange();
|
||||
});
|
||||
}
|
||||
|
||||
|
1049
src/main/java/org/qortal/controller/TradeBot.java
Normal file
1049
src/main/java/org/qortal/controller/TradeBot.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,28 +1,45 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.UTXO;
|
||||
import org.bitcoinj.core.UTXOProvider;
|
||||
import org.bitcoinj.core.UTXOProviderException;
|
||||
import org.bitcoinj.crypto.DeterministicHierarchy;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.utils.MonetaryFormat;
|
||||
import org.bitcoinj.wallet.DeterministicKeyChain;
|
||||
import org.bitcoinj.wallet.SendRequest;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.qortal.crosschain.ElectrumX.UnspentOutput;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class BTC {
|
||||
|
||||
public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
|
||||
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||
public static final int HASH160_LENGTH = 20;
|
||||
@ -30,6 +47,7 @@ public class BTC {
|
||||
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
|
||||
public enum BitcoinNet {
|
||||
MAIN {
|
||||
@ -58,6 +76,9 @@ public class BTC {
|
||||
private final NetworkParameters params;
|
||||
private final ElectrumX electrumX;
|
||||
|
||||
// Let ECKey.equals() do the hard work
|
||||
private final Set<ECKey> spentKeys = new HashSet<>();
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
private BTC() {
|
||||
@ -88,6 +109,34 @@ public class BTC {
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
public static String format(Coin amount) {
|
||||
return BTC.FORMAT.format(amount).toString();
|
||||
}
|
||||
|
||||
public static String format(long amount) {
|
||||
return format(Coin.valueOf(amount));
|
||||
}
|
||||
|
||||
public boolean isValidXprv(String xprv58) {
|
||||
try {
|
||||
DeterministicKey.deserializeB58(null, xprv58, this.params);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns P2PKH Bitcoin address using passed public key hash. */
|
||||
public String pkhToAddress(byte[] publicKeyHash) {
|
||||
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
|
||||
}
|
||||
|
||||
public String deriveP2shAddress(byte[] redeemScriptBytes) {
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
return p2shAddress.toString();
|
||||
}
|
||||
|
||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||
public Integer getMedianBlockTime() {
|
||||
Integer height = this.electrumX.getCurrentHeight();
|
||||
@ -99,34 +148,31 @@ public class BTC {
|
||||
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||
return null;
|
||||
|
||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||
|
||||
// Descending, but order shouldn't matter as we're picking median...
|
||||
// Descending order
|
||||
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
||||
|
||||
// Pick median
|
||||
return blockTimestamps.get(5);
|
||||
}
|
||||
|
||||
public Coin getBalance(String base58Address) {
|
||||
Long balance = this.electrumX.getBalance(addressToScript(base58Address));
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return Coin.valueOf(balance);
|
||||
public Long getBalance(String base58Address) {
|
||||
return this.electrumX.getBalance(addressToScript(base58Address));
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
|
||||
List<Pair<byte[], Integer>> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||
if (unspentOutputs == null)
|
||||
return null;
|
||||
|
||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA());
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
|
||||
if (transactionOutputs == null)
|
||||
return null;
|
||||
|
||||
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB()));
|
||||
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
|
||||
}
|
||||
|
||||
return unspentTransactionOutputs;
|
||||
@ -141,6 +187,7 @@ public class BTC {
|
||||
return transaction.getOutputs();
|
||||
}
|
||||
|
||||
/** Returns list of raw transactions spending passed address. */
|
||||
public List<byte[]> getAddressTransactions(String base58Address) {
|
||||
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
|
||||
}
|
||||
@ -149,6 +196,181 @@ public class BTC {
|
||||
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>.
|
||||
*
|
||||
* @param xprv58 BIP32 extended Bitcoin private key
|
||||
* @param recipient P2PKH address
|
||||
* @param amount unscaled amount
|
||||
* @return transaction, or null if insufficient funds
|
||||
*/
|
||||
public Transaction buildSpend(String xprv58, String recipient, long amount) {
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
|
||||
|
||||
Address destination = Address.fromString(this.params, recipient);
|
||||
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
|
||||
|
||||
if (this.params == TestNet3Params.get())
|
||||
// Much smaller fee for TestNet3
|
||||
sendRequest.feePerKb = Coin.valueOf(2000L);
|
||||
|
||||
try {
|
||||
wallet.completeTx(sendRequest);
|
||||
return sendRequest.tx;
|
||||
} catch (InsufficientMoneyException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unspent Bitcoin balance given 'm' BIP32 key.
|
||||
*
|
||||
* @param xprv58 BIP32 extended Bitcoin private key
|
||||
* @return unspent BTC balance, or null if unable to determine balance
|
||||
*/
|
||||
public Long getWalletBalance(String xprv58) {
|
||||
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
|
||||
|
||||
Coin balance = wallet.getBalance();
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return balance.value;
|
||||
}
|
||||
|
||||
// UTXOProvider support
|
||||
|
||||
static class WalletAwareUTXOProvider implements UTXOProvider {
|
||||
private static final int LOOKAHEAD_INCREMENT = 3;
|
||||
|
||||
private final BTC btc;
|
||||
private final Wallet wallet;
|
||||
|
||||
enum KeySearchMode {
|
||||
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
|
||||
}
|
||||
private final KeySearchMode keySearchMode;
|
||||
private final DeterministicKeyChain keyChain;
|
||||
|
||||
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) {
|
||||
this.btc = btc;
|
||||
this.wallet = wallet;
|
||||
this.keySearchMode = keySearchMode;
|
||||
this.keyChain = this.wallet.getActiveKeyChain();
|
||||
|
||||
// Set up wallet's key chain
|
||||
this.keyChain.setLookaheadSize(LOOKAHEAD_INCREMENT);
|
||||
this.keyChain.maybeLookAhead();
|
||||
}
|
||||
|
||||
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
|
||||
List<UTXO> allUnspentOutputs = new ArrayList<>();
|
||||
final boolean coinbase = false;
|
||||
|
||||
int ki = 0;
|
||||
do {
|
||||
boolean areAllKeysUnspent = true;
|
||||
boolean areAllKeysSpent = true;
|
||||
|
||||
for (; ki < keys.size(); ++ki) {
|
||||
ECKey key = keys.get(ki);
|
||||
|
||||
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
|
||||
if (unspentOutputs == null)
|
||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
||||
|
||||
/*
|
||||
* If there are no unspent outputs then either:
|
||||
* a) all the outputs have been spent
|
||||
* b) address has never been used
|
||||
*
|
||||
* For case (a) we want to remember not to check this address (key) again.
|
||||
*/
|
||||
|
||||
if (unspentOutputs.isEmpty()) {
|
||||
// If this is a known key that has been spent before, then we can skip asking for transaction history
|
||||
if (btc.spentKeys.contains(key)) {
|
||||
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
|
||||
areAllKeysUnspent = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
|
||||
if (historicTransactionHashes == null)
|
||||
throw new UTXOProviderException(
|
||||
String.format("Unable to fetch transaction history for %s", address));
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
// Fully spent key - case (a)
|
||||
btc.spentKeys.add(key);
|
||||
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
|
||||
areAllKeysUnspent = false;
|
||||
} else {
|
||||
// Key never been used - case (b)
|
||||
areAllKeysSpent = false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we reach here, then there's definitely at least one unspent key
|
||||
areAllKeysSpent = false;
|
||||
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
|
||||
if (transactionOutputs == null)
|
||||
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
|
||||
HashCode.fromBytes(unspentOutput.hash)));
|
||||
|
||||
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
|
||||
|
||||
UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
|
||||
Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
|
||||
transactionOutput.getScriptPubKey());
|
||||
|
||||
allUnspentOutputs.add(utxo);
|
||||
}
|
||||
}
|
||||
|
||||
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent)
|
||||
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
|
||||
// Generate some more keys
|
||||
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
|
||||
this.keyChain.maybeLookAhead();
|
||||
|
||||
// This returns all keys, including those already in 'keys'
|
||||
List<DeterministicKey> allLeafKeys = this.keyChain.getLeafKeys();
|
||||
// Add only new keys onto our list of keys to search
|
||||
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
|
||||
keys.addAll(newKeys);
|
||||
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
|
||||
}
|
||||
|
||||
// If we have processed all keys, then we're done
|
||||
} while (ki < keys.size());
|
||||
|
||||
return allUnspentOutputs;
|
||||
}
|
||||
|
||||
public int getChainHeadHeight() throws UTXOProviderException {
|
||||
Integer height = btc.electrumX.getCurrentHeight();
|
||||
if (height == null)
|
||||
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
|
||||
|
||||
return height.intValue();
|
||||
}
|
||||
|
||||
public NetworkParameters getParams() {
|
||||
return btc.params;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods for us
|
||||
|
||||
private byte[] addressToScript(String base58Address) {
|
||||
|
File diff suppressed because it is too large
Load Diff
236
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
236
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
@ -0,0 +1,236 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.Transaction.SigHash;
|
||||
import org.bitcoinj.core.TransactionInput;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
public class BTCP2SH {
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
|
||||
/*
|
||||
* OP_TUCK (to copy public key to before signature)
|
||||
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
|
||||
* OP_HASH160 (convert public key to PKH)
|
||||
* OP_DUP (duplicate PKH)
|
||||
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
|
||||
* OP_IF
|
||||
* OP_DROP (no need for duplicate PKH)
|
||||
* <push 4 bytes> <locktime>
|
||||
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
|
||||
* OP_ELSE
|
||||
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
|
||||
* OP_HASH160 (hash secret)
|
||||
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
|
||||
* OP_ENDIF
|
||||
*/
|
||||
|
||||
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
|
||||
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
|
||||
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
|
||||
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
|
||||
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
|
||||
|
||||
/**
|
||||
* Returns Bitcoin redeemScript used for cross-chain trading.
|
||||
* <p>
|
||||
* See comments in {@link BTCP2SH} for more details.
|
||||
*
|
||||
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
||||
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||
* @return
|
||||
*/
|
||||
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
|
||||
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
||||
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a custom transaction to spend P2SH.
|
||||
*
|
||||
* @param amount output amount, should be total of input amounts, less miner fees
|
||||
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @param lockTime (optional) transaction nLockTime, used in refund scenario
|
||||
* @param scriptSigBuilder function for building scriptSig using transaction input signature
|
||||
* @param outputPublicKeyHash PKH used to create P2PKH output
|
||||
* @return Signed Bitcoin transaction for spending P2SH
|
||||
*/
|
||||
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes,
|
||||
Long lockTime, Function<byte[], Script> scriptSigBuilder, byte[] outputPublicKeyHash) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
Transaction transaction = new Transaction(params);
|
||||
transaction.setVersion(2);
|
||||
|
||||
// Output is back to P2SH funder
|
||||
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(outputPublicKeyHash));
|
||||
|
||||
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
||||
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
|
||||
|
||||
// Input (without scriptSig prior to signing)
|
||||
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
|
||||
if (lockTime != null)
|
||||
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
|
||||
else
|
||||
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
|
||||
transaction.addInput(input);
|
||||
}
|
||||
|
||||
// Set locktime after inputs added but before input signatures are generated
|
||||
if (lockTime != null)
|
||||
transaction.setLockTime(lockTime);
|
||||
|
||||
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
||||
// Generate transaction signature for input
|
||||
final boolean anyoneCanPay = false;
|
||||
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||
|
||||
// Calculate transaction signature
|
||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
||||
|
||||
// Build scriptSig using lambda and tx signature
|
||||
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
|
||||
|
||||
// Set input scriptSig
|
||||
transaction.getInput(inputIndex).setScriptSig(scriptSig);
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns signed Bitcoin transaction claiming refund from P2SH address.
|
||||
*
|
||||
* @param refundAmount refund amount, should be total of input amounts, less miner fees
|
||||
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
|
||||
* @return Signed Bitcoin transaction for refunding P2SH
|
||||
*/
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
|
||||
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// transaction signature
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// redeem public key
|
||||
byte[] refundPubKey = refundKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
|
||||
|
||||
// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
return scriptBuilder.build();
|
||||
};
|
||||
|
||||
// Send funds back to funding address
|
||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
|
||||
*
|
||||
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
|
||||
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @param secret actual 32-byte secret used when building redeemScript
|
||||
* @param receivingAccountInfo Bitcoin PKH used for output
|
||||
* @return Signed Bitcoin transaction for redeeming P2SH
|
||||
*/
|
||||
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) {
|
||||
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// secret
|
||||
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
|
||||
|
||||
// transaction signature
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// redeem public key
|
||||
byte[] redeemPubKey = redeemKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
|
||||
|
||||
// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
return scriptBuilder.build();
|
||||
};
|
||||
|
||||
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
|
||||
}
|
||||
|
||||
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
|
||||
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
for (byte[] rawTransaction : rawTransactions) {
|
||||
Transaction transaction = new Transaction(params, rawTransaction);
|
||||
|
||||
// Cycle through inputs, looking for one that spends our P2SH
|
||||
for (TransactionInput input : transaction.getInputs()) {
|
||||
Script scriptSig = input.getScriptSig();
|
||||
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
||||
|
||||
// Expected number of script chunks for redeem. Refund might not have the same number.
|
||||
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
|
||||
if (scriptChunks.size() != expectedChunkCount)
|
||||
continue;
|
||||
|
||||
// We're expecting last chunk to contain the actual redeemScript
|
||||
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
|
||||
byte[] redeemScriptBytes = lastChunk.data;
|
||||
|
||||
// If non-push scripts, redeemScript will be null
|
||||
if (redeemScriptBytes == null)
|
||||
continue;
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!inputAddress.toString().equals(p2shAddress))
|
||||
// Input isn't spending our P2SH
|
||||
continue;
|
||||
|
||||
byte[] secret = scriptChunks.get(0).data;
|
||||
if (secret.length != BTCP2SH.SECRET_LENGTH)
|
||||
continue;
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -25,11 +25,11 @@ import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.TrustlessSSLSocketFactory;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */
|
||||
public class ElectrumX {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||
@ -93,7 +93,21 @@ public class ElectrumX {
|
||||
private ElectrumX(String bitcoinNetwork) {
|
||||
switch (bitcoinNetwork) {
|
||||
case "MAIN":
|
||||
servers.addAll(Arrays.asList());
|
||||
servers.addAll(Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002),
|
||||
new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002),
|
||||
new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002),
|
||||
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
|
||||
new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
|
||||
break;
|
||||
|
||||
case "TEST3":
|
||||
@ -119,6 +133,7 @@ public class ElectrumX {
|
||||
rpc("server.banner");
|
||||
}
|
||||
|
||||
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
|
||||
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
|
||||
if (!instances.containsKey(bitcoinNetwork))
|
||||
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
|
||||
@ -129,16 +144,26 @@ public class ElectrumX {
|
||||
// Methods for use by other classes
|
||||
|
||||
public Integer getCurrentHeight() {
|
||||
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe");
|
||||
if (blockJson == null || !blockJson.containsKey("height"))
|
||||
Object blockObj = this.rpc("blockchain.headers.subscribe");
|
||||
if (!(blockObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject blockJson = (JSONObject) blockObj;
|
||||
|
||||
if (!blockJson.containsKey("height"))
|
||||
return null;
|
||||
|
||||
return ((Long) blockJson.get("height")).intValue();
|
||||
}
|
||||
|
||||
public List<byte[]> getBlockHeaders(int startHeight, long count) {
|
||||
JSONObject blockJson = (JSONObject) this.rpc("blockchain.block.headers", startHeight, count);
|
||||
if (blockJson == null || !blockJson.containsKey("count") || !blockJson.containsKey("hex"))
|
||||
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
|
||||
if (!(blockObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject blockJson = (JSONObject) blockObj;
|
||||
|
||||
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
|
||||
return null;
|
||||
|
||||
Long returnedCount = (Long) blockJson.get("count");
|
||||
@ -155,57 +180,87 @@ public class ElectrumX {
|
||||
return rawBlockHeaders;
|
||||
}
|
||||
|
||||
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
|
||||
public Long getBalance(byte[] script) {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
JSONObject balanceJson = (JSONObject) this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
|
||||
if (balanceJson == null || !balanceJson.containsKey("confirmed"))
|
||||
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(balanceObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject balanceJson = (JSONObject) balanceObj;
|
||||
|
||||
if (!balanceJson.containsKey("confirmed"))
|
||||
return null;
|
||||
|
||||
return (Long) balanceJson.get("confirmed");
|
||||
}
|
||||
|
||||
public List<Pair<byte[], Integer>> getUnspentOutputs(byte[] script) {
|
||||
/** Unspent output info as returned by ElectrumX network. */
|
||||
public static class UnspentOutput {
|
||||
public final byte[] hash;
|
||||
public final int index;
|
||||
public final int height;
|
||||
public final long value;
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
this.height = height;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
|
||||
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
JSONArray unspentJson = (JSONArray) this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
|
||||
if (unspentJson == null)
|
||||
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(unspentJson instanceof JSONArray))
|
||||
return null;
|
||||
|
||||
List<Pair<byte[], Integer>> unspentOutputs = new ArrayList<>();
|
||||
for (Object rawUnspent : unspentJson) {
|
||||
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||
for (Object rawUnspent : (JSONArray) unspentJson) {
|
||||
JSONObject unspent = (JSONObject) rawUnspent;
|
||||
|
||||
int height = ((Long) unspent.get("height")).intValue();
|
||||
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
|
||||
if (height <= 0)
|
||||
continue;
|
||||
|
||||
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
|
||||
int outputIndex = ((Long) unspent.get("tx_pos")).intValue();
|
||||
long value = (Long) unspent.get("value");
|
||||
|
||||
unspentOutputs.add(new Pair<>(txHash, outputIndex));
|
||||
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value));
|
||||
}
|
||||
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
/** Returns raw transaction for passed transaction hash, or null if not found. */
|
||||
public byte[] getRawTransaction(byte[] txHash) {
|
||||
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
if (rawTransactionHex == null)
|
||||
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
|
||||
if (!(rawTransactionHex instanceof String))
|
||||
return null;
|
||||
|
||||
return HashCode.fromString(rawTransactionHex).asBytes();
|
||||
return HashCode.fromString((String) rawTransactionHex).asBytes();
|
||||
}
|
||||
|
||||
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
|
||||
public List<byte[]> getAddressTransactions(byte[] script) {
|
||||
byte[] scriptHash = Crypto.digest(script);
|
||||
Bytes.reverse(scriptHash);
|
||||
|
||||
JSONArray transactionsJson = (JSONArray) this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
|
||||
if (transactionsJson == null)
|
||||
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
|
||||
if (!(transactionsJson instanceof JSONArray))
|
||||
return null;
|
||||
|
||||
List<byte[]> rawTransactions = new ArrayList<>();
|
||||
|
||||
for (Object rawTransactionInfo : transactionsJson) {
|
||||
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
|
||||
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
|
||||
|
||||
// We only want confirmed transactions
|
||||
@ -223,6 +278,7 @@ public class ElectrumX {
|
||||
return rawTransactions;
|
||||
}
|
||||
|
||||
/** Returns true if raw transaction successfully broadcast. */
|
||||
public boolean broadcastTransaction(byte[] transactionBytes) {
|
||||
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
|
||||
if (rawBroadcastResult == null)
|
||||
@ -235,14 +291,15 @@ public class ElectrumX {
|
||||
|
||||
// Class-private utility methods
|
||||
|
||||
/** Query current server for its list of peer servers, and return those we can parse. */
|
||||
private Set<Server> serverPeersSubscribe() {
|
||||
Set<Server> newServers = new HashSet<>();
|
||||
|
||||
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe");
|
||||
if (peers == null)
|
||||
Object peers = this.connectedRpc("server.peers.subscribe");
|
||||
if (!(peers instanceof JSONArray))
|
||||
return newServers;
|
||||
|
||||
for (Object rawPeer : peers) {
|
||||
for (Object rawPeer : (JSONArray) peers) {
|
||||
JSONArray peer = (JSONArray) rawPeer;
|
||||
if (peer.size() < 3)
|
||||
continue;
|
||||
@ -287,6 +344,7 @@ public class ElectrumX {
|
||||
return newServers;
|
||||
}
|
||||
|
||||
/** Return output from RPC call, with automatic reconnection to different server if needed. */
|
||||
private synchronized Object rpc(String method, Object...params) {
|
||||
while (haveConnection()) {
|
||||
Object response = connectedRpc(method, params);
|
||||
@ -305,6 +363,7 @@ public class ElectrumX {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns true if we have, or create, a connection to an ElectrumX server. */
|
||||
private boolean haveConnection() {
|
||||
if (this.currentServer != null)
|
||||
return true;
|
||||
@ -377,10 +436,12 @@ public class ElectrumX {
|
||||
if (response.isEmpty())
|
||||
return null;
|
||||
|
||||
JSONObject responseJson = (JSONObject) JSONValue.parse(response);
|
||||
if (responseJson == null)
|
||||
Object responseObj = JSONValue.parse(response);
|
||||
if (!(responseObj instanceof JSONObject))
|
||||
return null;
|
||||
|
||||
JSONObject responseJson = (JSONObject) responseObj;
|
||||
|
||||
return responseJson.get("result");
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,14 @@ import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CrossChainTradeData {
|
||||
|
||||
public enum Mode { OFFER, TRADE };
|
||||
|
||||
// Properties
|
||||
|
||||
@Schema(description = "AT's Qortal address")
|
||||
@ -20,32 +20,40 @@ public class CrossChainTradeData {
|
||||
@Schema(description = "AT creator's Qortal address")
|
||||
public String qortalCreator;
|
||||
|
||||
@Schema(description = "AT creator's Qortal trade address")
|
||||
public String qortalCreatorTradeAddress;
|
||||
|
||||
@Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)")
|
||||
public byte[] creatorBitcoinPKH;
|
||||
|
||||
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
|
||||
public long creationTimestamp;
|
||||
|
||||
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
|
||||
public int tradeTimeout;
|
||||
|
||||
@Schema(description = "AT's current QORT balance")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long qortBalance;
|
||||
|
||||
@Schema(description = "HASH160 of 32-byte secret")
|
||||
public byte[] secretHash;
|
||||
@Schema(description = "HASH160 of 32-byte secret-A")
|
||||
public byte[] hashOfSecretA;
|
||||
|
||||
@Schema(description = "Initial QORT payment that will be sent to Qortal trade partner")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long initialPayout;
|
||||
@Schema(description = "HASH160 of 32-byte secret-B")
|
||||
public byte[] hashOfSecretB;
|
||||
|
||||
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long redeemPayout;
|
||||
public long qortAmount;
|
||||
|
||||
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
|
||||
public String qortalRecipient;
|
||||
public String qortalPartnerAddress;
|
||||
|
||||
@Schema(description = "Timestamp when AT switched to trade mode")
|
||||
public Long tradeModeTimestamp;
|
||||
|
||||
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
|
||||
public long tradeRefundTimeout;
|
||||
@Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
|
||||
public Integer refundTimeout;
|
||||
|
||||
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
||||
public Integer tradeRefundHeight;
|
||||
@ -54,10 +62,19 @@ public class CrossChainTradeData {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long expectedBitcoin;
|
||||
|
||||
public Mode mode;
|
||||
public BTCACCT.Mode mode;
|
||||
|
||||
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout")
|
||||
public Integer lockTime;
|
||||
@Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
|
||||
public Integer lockTimeA;
|
||||
|
||||
@Schema(description = "Suggested Bitcoin P2SH-B nLockTime based on trade timeout")
|
||||
public Integer lockTimeB;
|
||||
|
||||
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
|
||||
public byte[] partnerBitcoinPKH;
|
||||
|
||||
@Schema(description = "Trade partner's Qortal receiving address")
|
||||
public String qortalPartnerReceivingAddress;
|
||||
|
||||
// Constructors
|
||||
|
||||
|
193
src/main/java/org/qortal/data/crosschain/TradeBotData.java
Normal file
193
src/main/java/org/qortal/data/crosschain/TradeBotData.java
Normal file
@ -0,0 +1,193 @@
|
||||
package org.qortal.data.crosschain;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradeBotData {
|
||||
|
||||
private byte[] tradePrivateKey;
|
||||
|
||||
public enum State {
|
||||
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(15), BOB_WAITING_FOR_P2SH_B(20), BOB_WAITING_FOR_AT_REDEEM(25), BOB_DONE(30), BOB_REFUNDED(35),
|
||||
ALICE_WAITING_FOR_P2SH_A(80), ALICE_WAITING_FOR_AT_LOCK(85), ALICE_WATCH_P2SH_B(90), ALICE_DONE(95), ALICE_REFUNDING_B(100), ALICE_REFUNDING_A(105), ALICE_REFUNDED(110);
|
||||
|
||||
public final int value;
|
||||
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
State(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
}
|
||||
private State tradeState;
|
||||
|
||||
private String creatorAddress;
|
||||
private String atAddress;
|
||||
|
||||
private long timestamp;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long qortAmount;
|
||||
|
||||
private byte[] tradeNativePublicKey;
|
||||
private byte[] tradeNativePublicKeyHash;
|
||||
String tradeNativeAddress;
|
||||
|
||||
private byte[] secret;
|
||||
private byte[] hashOfSecret;
|
||||
|
||||
private byte[] tradeForeignPublicKey;
|
||||
private byte[] tradeForeignPublicKeyHash;
|
||||
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long bitcoinAmount;
|
||||
|
||||
// Never expose this via API
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private String xprv58;
|
||||
|
||||
private byte[] lastTransactionSignature;
|
||||
|
||||
private Integer lockTimeA;
|
||||
|
||||
// Could be Bitcoin or Qortal...
|
||||
private byte[] receivingAccountInfo;
|
||||
|
||||
protected TradeBotData() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public TradeBotData(byte[] tradePrivateKey, State tradeState, String creatorAddress, String atAddress,
|
||||
long timestamp, long qortAmount,
|
||||
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress,
|
||||
byte[] secret, byte[] hashOfSecret,
|
||||
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash,
|
||||
long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingAccountInfo) {
|
||||
this.tradePrivateKey = tradePrivateKey;
|
||||
this.tradeState = tradeState;
|
||||
this.creatorAddress = creatorAddress;
|
||||
this.atAddress = atAddress;
|
||||
this.timestamp = timestamp;
|
||||
this.qortAmount = qortAmount;
|
||||
this.tradeNativePublicKey = tradeNativePublicKey;
|
||||
this.tradeNativePublicKeyHash = tradeNativePublicKeyHash;
|
||||
this.tradeNativeAddress = tradeNativeAddress;
|
||||
this.secret = secret;
|
||||
this.hashOfSecret = hashOfSecret;
|
||||
this.tradeForeignPublicKey = tradeForeignPublicKey;
|
||||
this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash;
|
||||
this.bitcoinAmount = bitcoinAmount;
|
||||
this.xprv58 = xprv58;
|
||||
this.lastTransactionSignature = lastTransactionSignature;
|
||||
this.lockTimeA = lockTimeA;
|
||||
this.receivingAccountInfo = receivingAccountInfo;
|
||||
}
|
||||
|
||||
public byte[] getTradePrivateKey() {
|
||||
return this.tradePrivateKey;
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return this.tradeState;
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
this.tradeState = state;
|
||||
}
|
||||
|
||||
public String getCreatorAddress() {
|
||||
return this.creatorAddress;
|
||||
}
|
||||
|
||||
public String getAtAddress() {
|
||||
return this.atAddress;
|
||||
}
|
||||
|
||||
public void setAtAddress(String atAddress) {
|
||||
this.atAddress = atAddress;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getQortAmount() {
|
||||
return this.qortAmount;
|
||||
}
|
||||
|
||||
public byte[] getTradeNativePublicKey() {
|
||||
return this.tradeNativePublicKey;
|
||||
}
|
||||
|
||||
public byte[] getTradeNativePublicKeyHash() {
|
||||
return this.tradeNativePublicKeyHash;
|
||||
}
|
||||
|
||||
public String getTradeNativeAddress() {
|
||||
return this.tradeNativeAddress;
|
||||
}
|
||||
|
||||
public byte[] getSecret() {
|
||||
return this.secret;
|
||||
}
|
||||
|
||||
public byte[] getHashOfSecret() {
|
||||
return this.hashOfSecret;
|
||||
}
|
||||
|
||||
public byte[] getTradeForeignPublicKey() {
|
||||
return this.tradeForeignPublicKey;
|
||||
}
|
||||
|
||||
public byte[] getTradeForeignPublicKeyHash() {
|
||||
return this.tradeForeignPublicKeyHash;
|
||||
}
|
||||
|
||||
public long getBitcoinAmount() {
|
||||
return this.bitcoinAmount;
|
||||
}
|
||||
|
||||
public String getXprv58() {
|
||||
return this.xprv58;
|
||||
}
|
||||
|
||||
public byte[] getLastTransactionSignature() {
|
||||
return this.lastTransactionSignature;
|
||||
}
|
||||
|
||||
public void setLastTransactionSignature(byte[] lastTransactionSignature) {
|
||||
this.lastTransactionSignature = lastTransactionSignature;
|
||||
}
|
||||
|
||||
public Integer getLockTimeA() {
|
||||
return this.lockTimeA;
|
||||
}
|
||||
|
||||
public void setLockTimeA(Integer lockTimeA) {
|
||||
this.lockTimeA = lockTimeA;
|
||||
}
|
||||
|
||||
public byte[] getReceivingAccountInfo() {
|
||||
return this.receivingAccountInfo;
|
||||
}
|
||||
|
||||
}
|
5
src/main/java/org/qortal/event/Event.java
Normal file
5
src/main/java/org/qortal/event/Event.java
Normal file
@ -0,0 +1,5 @@
|
||||
package org.qortal.event;
|
||||
|
||||
public interface Event {
|
||||
|
||||
}
|
33
src/main/java/org/qortal/event/EventBus.java
Normal file
33
src/main/java/org/qortal/event/EventBus.java
Normal file
@ -0,0 +1,33 @@
|
||||
package org.qortal.event;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public enum EventBus {
|
||||
INSTANCE;
|
||||
|
||||
private static final List<Listener> LISTENERS = new ArrayList<>();
|
||||
|
||||
public void addListener(Listener newListener) {
|
||||
synchronized (LISTENERS) {
|
||||
LISTENERS.add(newListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
synchronized (LISTENERS) {
|
||||
LISTENERS.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void notify(Event event) {
|
||||
List<Listener> clonedListeners;
|
||||
|
||||
synchronized (LISTENERS) {
|
||||
clonedListeners = new ArrayList<>(LISTENERS);
|
||||
}
|
||||
|
||||
for (Listener listener : clonedListeners)
|
||||
listener.listen(event);
|
||||
}
|
||||
}
|
6
src/main/java/org/qortal/event/Listener.java
Normal file
6
src/main/java/org/qortal/event/Listener.java
Normal file
@ -0,0 +1,6 @@
|
||||
package org.qortal.event;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Listener {
|
||||
void listen(Event event);
|
||||
}
|
@ -15,6 +15,9 @@ public interface ATRepository {
|
||||
/** Returns where AT with passed address exists in repository */
|
||||
public boolean exists(String atAddress) throws DataException;
|
||||
|
||||
/** Returns AT creator's public key, or null if not found */
|
||||
public byte[] getCreatorPublicKey(String atAddress) throws DataException;
|
||||
|
||||
/** Returns list of executable ATs, empty if none found */
|
||||
public List<ATData> getAllExecutableATs() throws DataException;
|
||||
|
||||
@ -54,6 +57,24 @@ public interface ATRepository {
|
||||
*/
|
||||
public ATStateData getLatestATState(String atAddress) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns final ATStateData for ATs matching codeHash (required)
|
||||
* and specific data segment value (optional).
|
||||
* <p>
|
||||
* If searching for specific data segment value, both <tt>dataByteOffset</tt>
|
||||
* and <tt>expectedValue</tt> need to be non-null.
|
||||
* <p>
|
||||
* Note that <tt>dataByteOffset</tt> starts from 0 and will typically be
|
||||
* a multiple of <tt>MachineState.VALUE_SIZE</tt>, which is usually 8:
|
||||
* width of a long.
|
||||
* <p>
|
||||
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
|
||||
* the data segment comparison is done via unsigned hex string.
|
||||
*/
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns all ATStateData for a given block height.
|
||||
* <p>
|
||||
@ -88,4 +109,28 @@ public interface ATRepository {
|
||||
/** Delete state data for all ATs at this height */
|
||||
public void deleteATStates(int height) throws DataException;
|
||||
|
||||
// Finding transactions for ATs to process
|
||||
|
||||
static class NextTransactionInfo {
|
||||
public final int height;
|
||||
public final int sequence;
|
||||
public final byte[] signature;
|
||||
|
||||
public NextTransactionInfo(int height, int sequence, byte[] signature) {
|
||||
this.height = height;
|
||||
this.sequence = sequence;
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next transaction for AT to process.
|
||||
* <p>
|
||||
* @param recipient AT address
|
||||
* @param height starting height
|
||||
* @param sequence starting sequence
|
||||
* @return next transaction info, or null if none found
|
||||
*/
|
||||
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException;
|
||||
|
||||
}
|
||||
|
@ -61,6 +61,15 @@ public interface BlockRepository {
|
||||
*/
|
||||
public int getHeightFromTimestamp(long timestamp) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block timestamp for a given height.
|
||||
*
|
||||
* @param height
|
||||
* @return timestamp, or 0 if height is out of bounds.
|
||||
* @throws DataException
|
||||
*/
|
||||
public long getTimestampFromHeight(int height) throws DataException;
|
||||
|
||||
/**
|
||||
* Return highest block height from repository.
|
||||
*
|
||||
|
@ -0,0 +1,18 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
|
||||
public interface CrossChainRepository {
|
||||
|
||||
public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException;
|
||||
|
||||
public List<TradeBotData> getAllTradeBotData() throws DataException;
|
||||
|
||||
public void save(TradeBotData tradeBotData) throws DataException;
|
||||
|
||||
/** Delete trade-bot states using passed private key. */
|
||||
public int delete(byte[] tradePrivateKey) throws DataException;
|
||||
|
||||
}
|
@ -14,6 +14,8 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public ChatRepository getChatRepository();
|
||||
|
||||
public CrossChainRepository getCrossChainRepository();
|
||||
|
||||
public GroupRepository getGroupRepository();
|
||||
|
||||
public NameRepository getNameRepository();
|
||||
|
@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@ -107,6 +108,18 @@ public interface TransactionRepository {
|
||||
*/
|
||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of MESSAGE transaction data matching recipient.
|
||||
* @param recipient
|
||||
* @param limit
|
||||
* @param offset
|
||||
* @param reverse
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transactions relating to specific asset ID.
|
||||
*
|
||||
|
@ -68,6 +68,20 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCreatorPublicKey(String atAddress) throws DataException {
|
||||
String sql = "SELECT creator FROM ATs WHERE AT_address = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
return resultSet.getBytes(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT creator's public key from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATData> getAllExecutableATs() throws DataException {
|
||||
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
|
||||
@ -273,6 +287,78 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY height DESC "
|
||||
+ "LIMIT 1"
|
||||
+ ") AS FinalATStates "
|
||||
+ "WHERE code_hash = ? ");
|
||||
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
bindParams.add(codeHash);
|
||||
|
||||
if (isFinished != null) {
|
||||
sql.append("AND is_finished = ?");
|
||||
bindParams.add(isFinished);
|
||||
}
|
||||
|
||||
if (dataByteOffset != null && expectedValue != null) {
|
||||
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
|
||||
|
||||
// We convert our long to hex Java-side to control endian
|
||||
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
|
||||
|
||||
// SQL binary data offsets start at 1
|
||||
bindParams.add(dataByteOffset + 1);
|
||||
bindParams.add(expectedHexValue);
|
||||
}
|
||||
|
||||
if (minimumFinalHeight != null) {
|
||||
sql.append("AND height >= ");
|
||||
sql.append(minimumFinalHeight);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY height ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<ATStateData> atStates = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return atStates;
|
||||
|
||||
do {
|
||||
String atAddress = resultSet.getString(1);
|
||||
int height = resultSet.getInt(2);
|
||||
long created = resultSet.getLong(3);
|
||||
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
|
||||
byte[] stateHash = resultSet.getBytes(5);
|
||||
long fees = resultSet.getLong(6);
|
||||
boolean isInitial = resultSet.getBoolean(7);
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
|
||||
|
||||
atStates.add(atStateData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return atStates;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch matching AT states from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||
@ -341,4 +427,40 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// Finding transactions for ATs to process
|
||||
|
||||
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException {
|
||||
// We only need to search for a subset of transaction types: MESSAGE, PAYMENT or AT
|
||||
|
||||
String sql = "SELECT height, sequence, Transactions.signature "
|
||||
+ "FROM ("
|
||||
+ "SELECT signature FROM PaymentTransactions WHERE recipient = ? "
|
||||
+ "UNION "
|
||||
+ "SELECT signature FROM MessageTransactions WHERE recipient = ? "
|
||||
+ "UNION "
|
||||
+ "SELECT signature FROM ATTransactions WHERE recipient = ?"
|
||||
+ ") AS Transactions "
|
||||
+ "JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature "
|
||||
+ "JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature "
|
||||
+ "WHERE (height > ? OR (height = ? AND sequence > ?)) "
|
||||
+ "ORDER BY height ASC, sequence ASC "
|
||||
+ "LIMIT 1";
|
||||
|
||||
Object[] bindParams = new Object[] { recipient, recipient, recipient, height, height, sequence };
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int nextHeight = resultSet.getInt(1);
|
||||
int nextSequence = resultSet.getInt(2);
|
||||
byte[] nextSignature = resultSet.getBytes(3);
|
||||
|
||||
return new NextTransactionInfo(nextHeight, nextSequence, nextSignature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to find next transaction to AT from repository", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -132,6 +132,20 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestampFromHeight(int height) throws DataException {
|
||||
String sql = "SELECT minted_when FROM Blocks WHERE height = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) {
|
||||
if (resultSet == null)
|
||||
return 0;
|
||||
|
||||
return resultSet.getLong(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error obtaining block timestamp by height from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBlockchainHeight() throws DataException {
|
||||
String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1";
|
||||
|
@ -0,0 +1,165 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.repository.CrossChainRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
public class HSQLDBCrossChainRepository implements CrossChainRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
|
||||
public HSQLDBCrossChainRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException {
|
||||
String sql = "SELECT trade_state, creator_address, at_address, "
|
||||
+ "updated_when, qort_amount, "
|
||||
+ "trade_native_public_key, trade_native_public_key_hash, "
|
||||
+ "trade_native_address, secret, hash_of_secret, "
|
||||
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
|
||||
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info "
|
||||
+ "FROM TradeBotStates "
|
||||
+ "WHERE trade_private_key = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, tradePrivateKey)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int tradeStateValue = resultSet.getInt(1);
|
||||
TradeBotData.State tradeState = TradeBotData.State.valueOf(tradeStateValue);
|
||||
if (tradeState == null)
|
||||
throw new DataException("Illegal trade-bot trade-state fetched from repository");
|
||||
|
||||
String creatorAddress = resultSet.getString(2);
|
||||
String atAddress = resultSet.getString(3);
|
||||
long timestamp = resultSet.getLong(4);
|
||||
long qortAmount = resultSet.getLong(5);
|
||||
byte[] tradeNativePublicKey = resultSet.getBytes(6);
|
||||
byte[] tradeNativePublicKeyHash = resultSet.getBytes(7);
|
||||
String tradeNativeAddress = resultSet.getString(8);
|
||||
byte[] secret = resultSet.getBytes(9);
|
||||
byte[] hashOfSecret = resultSet.getBytes(10);
|
||||
byte[] tradeForeignPublicKey = resultSet.getBytes(11);
|
||||
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(12);
|
||||
long bitcoinAmount = resultSet.getLong(13);
|
||||
String xprv58 = resultSet.getString(14);
|
||||
byte[] lastTransactionSignature = resultSet.getBytes(15);
|
||||
Integer lockTimeA = resultSet.getInt(16);
|
||||
if (lockTimeA == 0 && resultSet.wasNull())
|
||||
lockTimeA = null;
|
||||
byte[] receivingAccountInfo = resultSet.getBytes(17);
|
||||
|
||||
return new TradeBotData(tradePrivateKey, tradeState,
|
||||
creatorAddress, atAddress, timestamp, qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
secret, hashOfSecret,
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot trading state from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TradeBotData> getAllTradeBotData() throws DataException {
|
||||
String sql = "SELECT trade_private_key, trade_state, creator_address, at_address, "
|
||||
+ "updated_when, qort_amount, "
|
||||
+ "trade_native_public_key, trade_native_public_key_hash, "
|
||||
+ "trade_native_address, secret, hash_of_secret, "
|
||||
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
|
||||
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info "
|
||||
+ "FROM TradeBotStates";
|
||||
|
||||
List<TradeBotData> allTradeBotData = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return allTradeBotData;
|
||||
|
||||
do {
|
||||
byte[] tradePrivateKey = resultSet.getBytes(1);
|
||||
int tradeStateValue = resultSet.getInt(2);
|
||||
TradeBotData.State tradeState = TradeBotData.State.valueOf(tradeStateValue);
|
||||
if (tradeState == null)
|
||||
throw new DataException("Illegal trade-bot trade-state fetched from repository");
|
||||
|
||||
String creatorAddress = resultSet.getString(3);
|
||||
String atAddress = resultSet.getString(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
long qortAmount = resultSet.getLong(6);
|
||||
byte[] tradeNativePublicKey = resultSet.getBytes(7);
|
||||
byte[] tradeNativePublicKeyHash = resultSet.getBytes(8);
|
||||
String tradeNativeAddress = resultSet.getString(9);
|
||||
byte[] secret = resultSet.getBytes(10);
|
||||
byte[] hashOfSecret = resultSet.getBytes(11);
|
||||
byte[] tradeForeignPublicKey = resultSet.getBytes(12);
|
||||
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(13);
|
||||
long bitcoinAmount = resultSet.getLong(14);
|
||||
String xprv58 = resultSet.getString(15);
|
||||
byte[] lastTransactionSignature = resultSet.getBytes(16);
|
||||
Integer lockTimeA = resultSet.getInt(17);
|
||||
if (lockTimeA == 0 && resultSet.wasNull())
|
||||
lockTimeA = null;
|
||||
byte[] receivingAccountInfo = resultSet.getBytes(18);
|
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
|
||||
creatorAddress, atAddress, timestamp, qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
secret, hashOfSecret,
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo);
|
||||
allTradeBotData.add(tradeBotData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return allTradeBotData;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot trading states from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(TradeBotData tradeBotData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("TradeBotStates");
|
||||
|
||||
saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey())
|
||||
.bind("trade_state", tradeBotData.getState().value)
|
||||
.bind("creator_address", tradeBotData.getCreatorAddress())
|
||||
.bind("at_address", tradeBotData.getAtAddress())
|
||||
.bind("updated_when", tradeBotData.getTimestamp())
|
||||
.bind("qort_amount", tradeBotData.getQortAmount())
|
||||
.bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey())
|
||||
.bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash())
|
||||
.bind("trade_native_address", tradeBotData.getTradeNativeAddress())
|
||||
.bind("secret", tradeBotData.getSecret()).bind("hash_of_secret", tradeBotData.getHashOfSecret())
|
||||
.bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey())
|
||||
.bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash())
|
||||
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
|
||||
.bind("xprv58", tradeBotData.getXprv58())
|
||||
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature())
|
||||
.bind("locktime_a", tradeBotData.getLockTimeA())
|
||||
.bind("receiving_account_info", tradeBotData.getReceivingAccountInfo());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save trade bot data into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(byte[] tradePrivateKey) throws DataException {
|
||||
try {
|
||||
return this.repository.delete("TradeBotStates", "trade_private_key = ?", tradePrivateKey);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete trade-bot states from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -4,9 +4,14 @@ import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class HSQLDBDatabaseUpdates {
|
||||
|
||||
@ -618,6 +623,22 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")");
|
||||
break;
|
||||
|
||||
case 20:
|
||||
// Trade bot
|
||||
stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, "
|
||||
+ "creator_address QortalAddress NOT NULL, at_address QortalAddress, updated_when BIGINT NOT NULL, qort_amount QortalAmount NOT NULL, "
|
||||
+ "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, "
|
||||
+ "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, "
|
||||
+ "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
|
||||
+ "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, "
|
||||
+ "receiving_account_info VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))");
|
||||
break;
|
||||
|
||||
case 21:
|
||||
// AT functionality index
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS ATCodeHashIndex ON ATs (code_hash, is_finished)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -32,6 +32,7 @@ import org.qortal.repository.ArbitraryRepository;
|
||||
import org.qortal.repository.AssetRepository;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.ChatRepository;
|
||||
import org.qortal.repository.CrossChainRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.NameRepository;
|
||||
@ -115,6 +116,11 @@ public class HSQLDBRepository implements Repository {
|
||||
return new HSQLDBChatRepository(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossChainRepository getCrossChainRepository() {
|
||||
return new HSQLDBCrossChainRepository(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupRepository getGroupRepository() {
|
||||
return new HSQLDBGroupRepository(this);
|
||||
|
@ -19,6 +19,7 @@ import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
@ -630,6 +631,43 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature from MessageTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||
+ "WHERE recipient = ?");
|
||||
|
||||
sql.append("ORDER BY Transactions.created_when");
|
||||
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
|
||||
if (resultSet == null)
|
||||
return messageTransactionsData;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
|
||||
TransactionData transactionData = this.fromSignature(signature);
|
||||
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
|
||||
return null;
|
||||
|
||||
messageTransactionsData.add((MessageTransactionData) transactionData);
|
||||
} while (resultSet.next());
|
||||
|
||||
return messageTransactionsData;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch trade-bot messages from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
|
@ -26,7 +26,7 @@ import com.google.common.base.Utf8;
|
||||
public class DeployAtTransaction extends Transaction {
|
||||
|
||||
// Properties
|
||||
private DeployAtTransactionData deployATTransactionData;
|
||||
private DeployAtTransactionData deployAtTransactionData;
|
||||
|
||||
// Other useful constants
|
||||
public static final int MAX_NAME_SIZE = 200;
|
||||
@ -40,31 +40,31 @@ public class DeployAtTransaction extends Transaction {
|
||||
public DeployAtTransaction(Repository repository, TransactionData transactionData) {
|
||||
super(repository, transactionData);
|
||||
|
||||
this.deployATTransactionData = (DeployAtTransactionData) this.transactionData;
|
||||
this.deployAtTransactionData = (DeployAtTransactionData) this.transactionData;
|
||||
}
|
||||
|
||||
// More information
|
||||
|
||||
@Override
|
||||
public List<String> getRecipientAddresses() throws DataException {
|
||||
return Collections.singletonList(this.deployATTransactionData.getAtAddress());
|
||||
return Collections.singletonList(this.deployAtTransactionData.getAtAddress());
|
||||
}
|
||||
|
||||
/** Returns AT version from the header bytes */
|
||||
private short getVersion() {
|
||||
byte[] creationBytes = deployATTransactionData.getCreationBytes();
|
||||
byte[] creationBytes = deployAtTransactionData.getCreationBytes();
|
||||
return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian
|
||||
}
|
||||
|
||||
/** Make sure deployATTransactionData has an ATAddress */
|
||||
private void ensureATAddress() throws DataException {
|
||||
if (this.deployATTransactionData.getAtAddress() != null)
|
||||
public static void ensureATAddress(DeployAtTransactionData deployAtTransactionData) throws DataException {
|
||||
if (deployAtTransactionData.getAtAddress() != null)
|
||||
return;
|
||||
|
||||
// Use transaction transformer
|
||||
try {
|
||||
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(this.deployATTransactionData));
|
||||
this.deployATTransactionData.setAtAddress(atAddress);
|
||||
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(deployAtTransactionData));
|
||||
deployAtTransactionData.setAtAddress(atAddress);
|
||||
} catch (TransformationException e) {
|
||||
throw new DataException("Unable to generate AT address");
|
||||
}
|
||||
@ -73,9 +73,9 @@ public class DeployAtTransaction extends Transaction {
|
||||
// Navigation
|
||||
|
||||
public Account getATAccount() throws DataException {
|
||||
ensureATAddress();
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
||||
return new Account(this.repository, this.deployATTransactionData.getAtAddress());
|
||||
return new Account(this.repository, this.deployAtTransactionData.getAtAddress());
|
||||
}
|
||||
|
||||
// Processing
|
||||
@ -83,30 +83,30 @@ public class DeployAtTransaction extends Transaction {
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Check name size bounds
|
||||
int nameLength = Utf8.encodedLength(this.deployATTransactionData.getName());
|
||||
int nameLength = Utf8.encodedLength(this.deployAtTransactionData.getName());
|
||||
if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
|
||||
return ValidationResult.INVALID_NAME_LENGTH;
|
||||
|
||||
// Check description size bounds
|
||||
int descriptionlength = Utf8.encodedLength(this.deployATTransactionData.getDescription());
|
||||
int descriptionlength = Utf8.encodedLength(this.deployAtTransactionData.getDescription());
|
||||
if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
|
||||
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
|
||||
|
||||
// Check AT-type size bounds
|
||||
int atTypeLength = Utf8.encodedLength(this.deployATTransactionData.getAtType());
|
||||
int atTypeLength = Utf8.encodedLength(this.deployAtTransactionData.getAtType());
|
||||
if (atTypeLength < 1 || atTypeLength > MAX_AT_TYPE_SIZE)
|
||||
return ValidationResult.INVALID_AT_TYPE_LENGTH;
|
||||
|
||||
// Check tags size bounds
|
||||
int tagsLength = Utf8.encodedLength(this.deployATTransactionData.getTags());
|
||||
int tagsLength = Utf8.encodedLength(this.deployAtTransactionData.getTags());
|
||||
if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
|
||||
return ValidationResult.INVALID_TAGS_LENGTH;
|
||||
|
||||
// Check amount is positive
|
||||
if (this.deployATTransactionData.getAmount() <= 0)
|
||||
if (this.deployAtTransactionData.getAmount() <= 0)
|
||||
return ValidationResult.NEGATIVE_AMOUNT;
|
||||
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
|
||||
// Check asset even exists
|
||||
if (assetData == null)
|
||||
@ -117,7 +117,7 @@ public class DeployAtTransaction extends Transaction {
|
||||
return ValidationResult.ASSET_NOT_SPENDABLE;
|
||||
|
||||
// Check asset amount is integer if asset is not divisible
|
||||
if (!assetData.isDivisible() && this.deployATTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
|
||||
if (!assetData.isDivisible() && this.deployAtTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
|
||||
return ValidationResult.INVALID_AMOUNT;
|
||||
|
||||
Account creator = this.getCreator();
|
||||
@ -125,15 +125,15 @@ public class DeployAtTransaction extends Transaction {
|
||||
// Check creator has enough funds
|
||||
if (assetId == Asset.QORT) {
|
||||
// Simple case: amount and fee both in QORT
|
||||
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
|
||||
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
|
||||
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
} else {
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
}
|
||||
|
||||
@ -142,12 +142,12 @@ public class DeployAtTransaction extends Transaction {
|
||||
return ValidationResult.INVALID_CREATION_BYTES;
|
||||
|
||||
// Check creation bytes are valid (for v2+)
|
||||
this.ensureATAddress();
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
||||
// Just enough AT data to allow API to query initial balances, etc.
|
||||
String atAddress = this.deployATTransactionData.getAtAddress();
|
||||
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
|
||||
long creation = this.deployATTransactionData.getTimestamp();
|
||||
String atAddress = this.deployAtTransactionData.getAtAddress();
|
||||
byte[] creatorPublicKey = this.deployAtTransactionData.getCreatorPublicKey();
|
||||
long creation = this.deployAtTransactionData.getTimestamp();
|
||||
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
|
||||
|
||||
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
|
||||
@ -157,7 +157,7 @@ public class DeployAtTransaction extends Transaction {
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
try {
|
||||
new MachineState(api, loggerFactory, this.deployATTransactionData.getCreationBytes());
|
||||
new MachineState(api, loggerFactory, this.deployAtTransactionData.getCreationBytes());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Not valid
|
||||
return ValidationResult.INVALID_CREATION_BYTES;
|
||||
@ -169,25 +169,25 @@ public class DeployAtTransaction extends Transaction {
|
||||
@Override
|
||||
public ValidationResult isProcessable() throws DataException {
|
||||
Account creator = getCreator();
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
|
||||
// Check creator has enough funds
|
||||
if (assetId == Asset.QORT) {
|
||||
// Simple case: amount and fee both in QORT
|
||||
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
|
||||
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
|
||||
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
} else {
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
|
||||
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
}
|
||||
|
||||
// Check AT doesn't already exist
|
||||
if (this.repository.getATRepository().exists(this.deployATTransactionData.getAtAddress()))
|
||||
if (this.repository.getATRepository().exists(this.deployAtTransactionData.getAtAddress()))
|
||||
return ValidationResult.AT_ALREADY_EXISTS;
|
||||
|
||||
return ValidationResult.OK;
|
||||
@ -195,40 +195,40 @@ public class DeployAtTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
this.ensureATAddress();
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
||||
// Deploy AT, saving into repository
|
||||
AT at = new AT(this.repository, this.deployATTransactionData);
|
||||
AT at = new AT(this.repository, this.deployAtTransactionData);
|
||||
at.deploy();
|
||||
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
|
||||
// Update creator's balance regarding initial payment to AT
|
||||
Account creator = getCreator();
|
||||
creator.modifyAssetBalance(assetId, - this.deployATTransactionData.getAmount());
|
||||
creator.modifyAssetBalance(assetId, - this.deployAtTransactionData.getAmount());
|
||||
|
||||
// Update AT's reference, which also creates AT account
|
||||
Account atAccount = this.getATAccount();
|
||||
atAccount.setLastReference(this.deployATTransactionData.getSignature());
|
||||
atAccount.setLastReference(this.deployAtTransactionData.getSignature());
|
||||
|
||||
// Update AT's balance
|
||||
atAccount.setConfirmedBalance(assetId, this.deployATTransactionData.getAmount());
|
||||
atAccount.setConfirmedBalance(assetId, this.deployAtTransactionData.getAmount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
// Delete AT from repository
|
||||
AT at = new AT(this.repository, this.deployATTransactionData);
|
||||
AT at = new AT(this.repository, this.deployAtTransactionData);
|
||||
at.undeploy();
|
||||
|
||||
long assetId = this.deployATTransactionData.getAssetId();
|
||||
long assetId = this.deployAtTransactionData.getAssetId();
|
||||
|
||||
// Update creator's balance regarding initial payment to AT
|
||||
Account creator = getCreator();
|
||||
creator.modifyAssetBalance(assetId, this.deployATTransactionData.getAmount());
|
||||
creator.modifyAssetBalance(assetId, this.deployAtTransactionData.getAmount());
|
||||
|
||||
// Delete AT's account (and hence its balance)
|
||||
this.repository.getAccountRepository().delete(this.deployATTransactionData.getAtAddress());
|
||||
this.repository.getAccountRepository().delete(this.deployAtTransactionData.getAtAddress());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,9 +26,26 @@ public class BitTwiddling {
|
||||
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
|
||||
}
|
||||
|
||||
/** Convert int to big-endian byte array */
|
||||
public static byte[] toBEByteArray(int value) {
|
||||
return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) };
|
||||
}
|
||||
|
||||
/** Convert long to big-endian byte array */
|
||||
public static byte[] toBEByteArray(long value) {
|
||||
return new byte[] { (byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32),
|
||||
(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) };
|
||||
}
|
||||
|
||||
/** Convert little-endian bytes to int */
|
||||
public static int fromLEBytes(byte[] bytes, int offset) {
|
||||
public static int intFromLEBytes(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xff) | (bytes[offset + 1] & 0xff) << 8 | (bytes[offset + 2] & 0xff) << 16 | (bytes[offset + 3] & 0xff) << 24;
|
||||
}
|
||||
|
||||
/** Convert big-endian bytes to long */
|
||||
public static long longFromBEBytes(byte[] bytes, int start) {
|
||||
return (bytes[start] & 0xffL) << 56 | (bytes[start + 1] & 0xffL) << 48 | (bytes[start + 2] & 0xffL) << 40 | (bytes[start + 3] & 0xffL) << 32
|
||||
| (bytes[start + 4] & 0xffL) << 24 | (bytes[start + 5] & 0xffL) << 16 | (bytes[start + 6] & 0xffL) << 8 | (bytes[start + 7] & 0xffL);
|
||||
}
|
||||
|
||||
}
|
||||
|
223
src/test/java/org/qortal/test/at/GetMessageLengthTests.java
Normal file
223
src/test/java/org/qortal/test/at/GetMessageLengthTests.java
Normal file
@ -0,0 +1,223 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Random;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.AccountUtils;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
public class GetMessageLengthTests extends Common {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
byte[] creationBytes = buildMessageLengthAT();
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Send messages with known length
|
||||
checkMessageLength(repository, deployer, atAddress, 1);
|
||||
checkMessageLength(repository, deployer, atAddress, 10);
|
||||
checkMessageLength(repository, deployer, atAddress, 32);
|
||||
checkMessageLength(repository, deployer, atAddress, 99);
|
||||
|
||||
// Finally, send a payment instead and check returned length is -1
|
||||
AccountUtils.pay(repository, deployer, atAddress, 123L);
|
||||
// Mint another block so AT can process payment
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
|
||||
|
||||
assertEquals(-1L, extractedLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkMessageLength(Repository repository, PrivateKeyAccount sender, String atAddress, int messageLength) throws DataException {
|
||||
byte[] testMessage = new byte[messageLength];
|
||||
RANDOM.nextBytes(testMessage);
|
||||
|
||||
sendMessage(repository, sender, testMessage, atAddress);
|
||||
// Mint another block so AT can process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
|
||||
|
||||
assertEquals(messageLength, extractedLength);
|
||||
}
|
||||
|
||||
private byte[] buildMessageLengthAT() {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Make result first for easier extraction
|
||||
final int addrResult = addrCounter++;
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// Code labels
|
||||
Integer labelCheckTx = null;
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckTx = codeByteBuffer.position();
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
|
||||
// Save message length
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrResult));
|
||||
|
||||
// Stop and wait for next block (and hence more transactions)
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(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);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = sender.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
int version = 4;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
Long assetId = null; // because amount is zero
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
}
|
268
src/test/java/org/qortal/test/at/GetNextTransactionTests.java
Normal file
268
src/test/java/org/qortal/test/at/GetNextTransactionTests.java
Normal file
@ -0,0 +1,268 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
public class GetNextTransactionTests extends Common {
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNextTransaction() throws DataException {
|
||||
byte[] data = new byte[] { 0x44 };
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
byte[] creationBytes = buildGetNextTransactionAT();
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
byte[] rawNextTimestamp = new byte[32];
|
||||
Transaction transaction;
|
||||
|
||||
// Confirm initial value is zero
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
|
||||
// Send message to someone other than AT
|
||||
sendMessage(repository, deployer, data, deployer.getAddress());
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT does not find message
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
|
||||
// Send message to AT
|
||||
transaction = sendMessage(repository, deployer, data, atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT finds message
|
||||
BlockUtils.mintBlock(repository);
|
||||
assertTimestamp(repository, atAddress, transaction);
|
||||
|
||||
// Mint a few blocks, then send non-AT message, followed by AT message
|
||||
for (int i = 0; i < 5; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
sendMessage(repository, deployer, data, deployer.getAddress());
|
||||
transaction = sendMessage(repository, deployer, data, atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT finds message
|
||||
BlockUtils.mintBlock(repository);
|
||||
assertTimestamp(repository, atAddress, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildGetNextTransactionAT() {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Beginning of data segment for easy extraction
|
||||
final int addrNextTx = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrNextTxIndex = addrCounter++;
|
||||
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// skip addrNextTx
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
|
||||
|
||||
// Store pointer to addrNextTx at addrNextTxIndex
|
||||
dataByteBuffer.putLong(addrNextTx);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
|
||||
// Stop if timestamp part of A is zero
|
||||
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(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);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
private void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
|
||||
}
|
||||
|
||||
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = sender.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
int version = 4;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
Long assetId = null; // because amount is zero
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndImportValid(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
|
||||
int height = transaction.getHeight();
|
||||
byte[] transactionSignature = transaction.getTransactionData().getSignature();
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
assertNotNull(blockData);
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
List<Transaction> blockTransactions = block.getTransactions();
|
||||
int sequence;
|
||||
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
|
||||
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
|
||||
break;
|
||||
|
||||
assertNotSame(-1, sequence);
|
||||
|
||||
byte[] rawNextTimestamp = new byte[32];
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
|
||||
Timestamp expectedTimestamp = new Timestamp(height, sequence);
|
||||
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
|
||||
|
||||
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
|
||||
height, sequence,
|
||||
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
|
||||
),
|
||||
expectedTimestamp.longValue(),
|
||||
actualTimestamp.longValue());
|
||||
|
||||
byte[] expectedPartialSignature = new byte[24];
|
||||
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
|
||||
|
||||
byte[] actualPartialSignature = new byte[24];
|
||||
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
|
||||
|
||||
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
|
||||
}
|
||||
|
||||
}
|
221
src/test/java/org/qortal/test/at/GetPartialMessageTests.java
Normal file
221
src/test/java/org/qortal/test/at/GetPartialMessageTests.java
Normal file
@ -0,0 +1,221 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalAtLoggerFactory;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
|
||||
public class GetPartialMessageTests extends Common {
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPartialMessage() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
byte[] messageData = "The quick brown fox jumped over the lazy dog.".getBytes();
|
||||
int[] offsets = new int[] { 0, 7, 32, 44, messageData.length };
|
||||
|
||||
byte[] creationBytes = buildGetPartialMessageAT(offsets);
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
sendMessage(repository, deployer, messageData, atAddress);
|
||||
|
||||
for (int offset : offsets) {
|
||||
// Mint another block so AT can process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
byte[] expectedData = new byte[32];
|
||||
int byteCount = Math.min(32, messageData.length - offset);
|
||||
System.arraycopy(messageData, offset, expectedData, 0, byteCount);
|
||||
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
|
||||
|
||||
byte[] actualData = new byte[32];
|
||||
System.arraycopy(dataBytes, MachineState.VALUE_SIZE, actualData, 0, 32);
|
||||
|
||||
assertArrayEquals(expectedData, actualData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildGetPartialMessageAT(int... offsets) {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
final int addrCopyOfBIndex = addrCounter++;
|
||||
|
||||
// 2nd position for easy extraction
|
||||
final int addrCopyOfB = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrResult = addrCounter++;
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
final int addrOffset = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
dataByteBuffer.putLong(addrCopyOfB);
|
||||
|
||||
// Code labels
|
||||
Integer labelCheckTx = null;
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckTx = codeByteBuffer.position();
|
||||
|
||||
// Generate code per offset
|
||||
for (int i = 0; i < offsets.length; ++i) {
|
||||
if (i > 0)
|
||||
// Wait for next block
|
||||
codeByteBuffer.put(OpCode.SLP_IMD.compile());
|
||||
|
||||
// Set offset
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrOffset, offsets[i]));
|
||||
|
||||
// Extract partial message
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOffset));
|
||||
|
||||
// Copy B to data segment
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCopyOfBIndex));
|
||||
}
|
||||
|
||||
// We're done
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(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);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = sender.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
int version = 4;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
Long assetId = null; // because amount is zero
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package org.qortal.test.btcacct;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@ -10,14 +9,15 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
@ -43,14 +43,18 @@ import com.google.common.primitives.Bytes;
|
||||
|
||||
public class AtTests extends Common {
|
||||
|
||||
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final int refundTimeout = 10; // blocks
|
||||
public static final long initialPayout = 100000L;
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
|
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
|
||||
public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
|
||||
public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
|
||||
public static final int tradeTimeout = 20; // blocks
|
||||
public static final long redeemAmount = 80_40200000L;
|
||||
public static final long fundingAmount = 123_45600000L;
|
||||
public static final long bitcoinAmount = 864200L;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
@ -58,9 +62,9 @@ public class AtTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testCompile() {
|
||||
Account deployer = Common.getTestAccount(null, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null);
|
||||
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
}
|
||||
|
||||
@ -68,12 +72,14 @@ public class AtTests extends Common {
|
||||
public void testDeploy() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
@ -85,10 +91,10 @@ public class AtTests extends Common {
|
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
@ -103,10 +109,10 @@ public class AtTests extends Common {
|
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
expectedBalance = partnersInitialBalance;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,26 +121,39 @@ public class AtTests extends Common {
|
||||
public void testOfferCancel() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Send creator's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = BTCACCT.buildCancelMessage(deployer.getAddress());
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// Refund should happen 1st block after receiving recipient address
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
|
||||
|
||||
@ -143,11 +162,10 @@ public class AtTests extends Common {
|
||||
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
|
||||
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee;
|
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
@ -157,71 +175,144 @@ public class AtTests extends Common {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testInitialPayment() throws DataException {
|
||||
public void testOfferCancelInvalidLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Instead of sending creator's address to AT, send too-short/invalid message
|
||||
byte[] messageData = new byte[7];
|
||||
RANDOM.nextBytes(messageData);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
expectedBalance = recipientsInitialBalance;
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testTradingInfoProcessing() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
|
||||
// Check hashOfSecretA was extracted correctly
|
||||
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
|
||||
|
||||
// Check trade partner Qortal address was extracted correctly
|
||||
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
|
||||
|
||||
// Check trade partner's Bitcoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
|
||||
}
|
||||
}
|
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectTradeSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT BUT NOT FROM AT CREATOR
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// Initial payment should NOT happen
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(BTCACCT.Mode.OFFERING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,34 +321,48 @@ public class AtTests extends Common {
|
||||
public void testAutomaticTradeRefund() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
BlockUtils.mintBlock(repository);
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
|
||||
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
|
||||
|
||||
// Check refund
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
long messageFee = messageTransaction.getTransactionData().getFee();
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee;
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode);
|
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance;
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
@ -267,46 +372,63 @@ public class AtTests extends Common {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretCorrectSender() throws DataException {
|
||||
public void testCorrectSecretsCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT
|
||||
messageTransaction = sendMessage(repository, recipient, secret, atAddress);
|
||||
// Send correct secrets to AT, from correct account
|
||||
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertTrue(atData.getIsFinished());
|
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.REDEEMED, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
@ -317,99 +439,206 @@ public class AtTests extends Common {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretIncorrectSender() throws DataException {
|
||||
public void testCorrectSecretsIncorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageTransaction = sendMessage(repository, bystander, secret, atAddress);
|
||||
// Send correct secrets to AT, but from wrong account
|
||||
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance;
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testIncorrectSecretCorrectSender() throws DataException {
|
||||
public void testIncorrectSecretsCorrectSender() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
|
||||
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
// Send recipient's address to AT
|
||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Initial payment should happen 1st block after receiving recipient address
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
byte[] wrongSecret = Crypto.digest(secret);
|
||||
messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress);
|
||||
// Send incorrect secrets to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32];
|
||||
RANDOM.nextBytes(wrongSecret);
|
||||
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Send incorrect secrets to AT, from correct account
|
||||
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should still be in TRADE mode
|
||||
tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
|
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
|
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testCorrectSecretsCorrectSenderInvalidMessageLength() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
Account at = deployAtTransaction.getATAccount();
|
||||
String atAddress = at.getAddress();
|
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
|
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
|
||||
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
|
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
|
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send correct secrets to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA, secretB);
|
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
|
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
describeAt(repository, atAddress);
|
||||
|
||||
// Check AT is NOT finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
assertFalse(atData.getIsFinished());
|
||||
|
||||
// AT should be in TRADING mode
|
||||
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
|
||||
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Test
|
||||
public void testDescribeDeployed() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
|
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
|
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
|
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
|
||||
|
||||
@ -433,8 +662,12 @@ public class AtTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
||||
private int calcTestLockTimeA(long messageTimestamp) {
|
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
@ -493,6 +726,7 @@ public class AtTests extends Common {
|
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
|
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
|
||||
int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
|
||||
@ -500,7 +734,7 @@ public class AtTests extends Common {
|
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - initialPayout;
|
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
@ -516,40 +750,43 @@ public class AtTests extends Common {
|
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
|
||||
System.out.print(String.format("%s:\n"
|
||||
+ "\tmode: %s\n"
|
||||
+ "\tcreator: %s,\n"
|
||||
+ "\tcreation timestamp: %s,\n"
|
||||
+ "\tcurrent balance: %s QORT,\n"
|
||||
+ "\tHASH160 of secret: %s,\n"
|
||||
+ "\tinitial payout: %s QORT,\n"
|
||||
+ "\tis finished: %b,\n"
|
||||
+ "\tHASH160 of secret-B: %s,\n"
|
||||
+ "\tredeem payout: %s QORT,\n"
|
||||
+ "\texpected bitcoin: %s BTC,\n"
|
||||
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
||||
+ "\tcurrent block height: %d,\n",
|
||||
tradeData.qortalAtAddress,
|
||||
tradeData.mode.name(),
|
||||
tradeData.qortalCreator,
|
||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||
Amounts.prettyAmount(tradeData.qortBalance),
|
||||
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.initialPayout),
|
||||
Amounts.prettyAmount(tradeData.redeemPayout),
|
||||
atData.getIsFinished(),
|
||||
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
|
||||
Amounts.prettyAmount(tradeData.qortAmount),
|
||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||
tradeData.tradeRefundTimeout,
|
||||
currentBlockHeight));
|
||||
|
||||
// Are we in 'offer' or 'trade' stage?
|
||||
if (tradeData.tradeRefundHeight == null) {
|
||||
// Offer
|
||||
System.out.println(String.format("\tstatus: 'offer mode'"));
|
||||
} else {
|
||||
// Trade
|
||||
System.out.println(String.format("\tstatus: 'trade mode',\n"
|
||||
+ "\ttrade timeout: block %d,\n"
|
||||
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
|
||||
+ "\ttrade recipient: %s",
|
||||
if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
|
||||
System.out.println(String.format("\trefund height: block %d,\n"
|
||||
+ "\tHASH160 of secret-A: %s,\n"
|
||||
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
|
||||
+ "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
|
||||
+ "\ttrade partner: %s",
|
||||
tradeData.tradeRefundHeight,
|
||||
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L),
|
||||
tradeData.qortalRecipient));
|
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
|
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
|
||||
tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L),
|
||||
tradeData.qortalPartnerAddress));
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) {
|
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,12 +5,13 @@ import static org.junit.Assert.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
@ -55,11 +56,52 @@ public class BtcTests extends Common {
|
||||
|
||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
||||
|
||||
byte[] expectedSecret = AtTests.secret;
|
||||
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
|
||||
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
|
||||
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
|
||||
|
||||
assertNotNull(secret);
|
||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildSpend() {
|
||||
BTC btc = BTC.getInstance();
|
||||
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = btc.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
transaction = btc.buildSpend(xprv58, recipient, amount);
|
||||
assertNotNull(transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
BTC btc = BTC.getInstance();
|
||||
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = btc.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(balance);
|
||||
|
||||
System.out.println(BTC.format(balance));
|
||||
|
||||
// Check spent key caching doesn't affect outcome
|
||||
|
||||
Long repeatBalance = btc.getWalletBalance(xprv58);
|
||||
|
||||
assertNotNull(repeatBalance);
|
||||
|
||||
System.out.println(BTC.format(repeatBalance));
|
||||
|
||||
assertEquals(balance, repeatBalance);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@ -98,12 +98,12 @@ public class BuildP2SH {
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@ -115,7 +115,7 @@ public class BuildP2SH {
|
||||
|
||||
// Fund P2SH
|
||||
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
|
||||
p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
p2shAddress.toString(), BTC.format(bitcoinAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
|
||||
} catch (DataException e) {
|
||||
|
@ -15,7 +15,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@ -106,14 +106,14 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@ -135,12 +135,12 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.FORMAT.format(p2shBalance)));
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
@ -152,7 +152,7 @@ public class CheckP2SH {
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
|
@ -34,20 +34,20 @@ public class DeployAT {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <trade-timeout>"));
|
||||
System.err.println(String.format("example: DeployAT "
|
||||
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
||||
+ "\t80.4020 \\\n"
|
||||
+ "\t0.00864200 \\\n"
|
||||
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t0.0001 \\\n"
|
||||
+ "\t123.456 \\\n"
|
||||
+ "\t10"));
|
||||
+ "\t10080"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 8)
|
||||
if (args.length != 7)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
@ -56,8 +56,8 @@ public class DeployAT {
|
||||
byte[] refundPrivateKey = null;
|
||||
long redeemAmount = 0;
|
||||
long expectedBitcoin = 0;
|
||||
byte[] bitcoinPublicKeyHash = null;
|
||||
byte[] secretHash = null;
|
||||
long initialPayout = 0;
|
||||
long fundingAmount = 0;
|
||||
int tradeTimeout = 0;
|
||||
|
||||
@ -75,21 +75,21 @@ public class DeployAT {
|
||||
if (expectedBitcoin <= 0)
|
||||
usage("Expected BTC amount must be positive");
|
||||
|
||||
bitcoinPublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (bitcoinPublicKeyHash.length != 20)
|
||||
usage("Bitcoin PKH must be 20 bytes");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
initialPayout = Long.parseLong(args[argIndex++]);
|
||||
if (initialPayout < 0)
|
||||
usage("Initial QORT payout must be positive");
|
||||
|
||||
fundingAmount = Long.parseLong(args[argIndex++]);
|
||||
if (fundingAmount <= redeemAmount)
|
||||
usage("AT funding amount must be greater than QORT redeem amount");
|
||||
|
||||
tradeTimeout = Integer.parseInt(args[argIndex++]);
|
||||
if (tradeTimeout < 10 || tradeTimeout > 50000)
|
||||
usage("AT trade timeout should be between 10 and 50,000 minutes");
|
||||
if (tradeTimeout < 60 || tradeTimeout > 50000)
|
||||
usage("Trade timeout (minutes) must be between 60 and 50000");
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
@ -114,7 +114,7 @@ public class DeployAT {
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
// Deploy AT
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
|
@ -12,8 +12,8 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crosschain.ElectrumX;
|
||||
import org.qortal.crosschain.ElectrumX.UnspentOutput;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
import org.qortal.utils.Pair;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
@ -61,7 +61,7 @@ public class ElectrumXTests {
|
||||
|
||||
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
|
||||
int offset = 4 + 32 + 32;
|
||||
int timestamp = BitTwiddling.fromLEBytes(blockHeader, offset);
|
||||
int timestamp = BitTwiddling.intFromLEBytes(blockHeader, offset);
|
||||
System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp));
|
||||
}
|
||||
}
|
||||
@ -100,13 +100,13 @@ public class ElectrumXTests {
|
||||
|
||||
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
List<Pair<byte[], Integer>> unspentOutputs = electrumX.getUnspentOutputs(script);
|
||||
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
|
||||
|
||||
assertNotNull(unspentOutputs);
|
||||
assertFalse(unspentOutputs.isEmpty());
|
||||
|
||||
for (Pair<byte[], Integer> unspentOutput : unspentOutputs)
|
||||
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.getA()).toString(), unspentOutput.getB()));
|
||||
for (UnspentOutput unspentOutput : unspentOutputs)
|
||||
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.hash), unspentOutput.index));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@ -107,7 +107,7 @@ public class Redeem {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
|
||||
// New/derived info
|
||||
@ -121,7 +121,7 @@ public class Redeem {
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@ -147,12 +147,12 @@ public class Redeem {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
@ -164,7 +164,7 @@ public class Redeem {
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
@ -179,10 +179,10 @@ public class Redeem {
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin redeemAmount = p2shBalance.subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
|
||||
Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash());
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
|
@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crosschain.BTCP2SH;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@ -110,7 +110,7 @@ public class Refund {
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Refund miner's fee: %s", BTC.format(bitcoinFee)));
|
||||
|
||||
// New/derived info
|
||||
|
||||
@ -120,7 +120,7 @@ public class Refund {
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
@ -151,12 +151,12 @@ public class Refund {
|
||||
}
|
||||
|
||||
// Check P2SH is funded
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||
@ -168,7 +168,7 @@ public class Refund {
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
|
||||
@ -183,10 +183,10 @@ public class Refund {
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin refundAmount = p2shBalance.subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
|
@ -20,15 +20,19 @@ public class AccountUtils {
|
||||
public static final int txGroupId = Group.NO_GROUP;
|
||||
public static final long fee = 1L * Amounts.MULTIPLIER;
|
||||
|
||||
public static void pay(Repository repository, String sender, String recipient, long amount) throws DataException {
|
||||
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, sender);
|
||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
|
||||
public static void pay(Repository repository, String testSenderName, String testRecipientName, long amount) throws DataException {
|
||||
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, testSenderName);
|
||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, testRecipientName);
|
||||
|
||||
pay(repository, sendingAccount, recipientAccount.getAddress(), amount);
|
||||
}
|
||||
|
||||
public static void pay(Repository repository, PrivateKeyAccount sendingAccount, String recipientAddress, long amount) throws DataException {
|
||||
byte[] reference = sendingAccount.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null);
|
||||
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAccount.getAddress(), amount);
|
||||
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAddress, amount);
|
||||
|
||||
TransactionUtils.signAndMint(repository, transactionData, sendingAccount);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user