Merge branch 'trade-bot'

This commit is contained in:
catbref 2020-08-06 09:00:45 +01:00
commit 47679b7f6c
60 changed files with 5503 additions and 892 deletions

View File

@ -4,6 +4,6 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId> <groupId>org.ciyam</groupId>
<artifactId>AT</artifactId> <artifactId>AT</artifactId>
<version>1.3.4</version> <version>1.3.5</version>
<description>POM was created from install:install-file</description> <description>POM was created from install:install-file</description>
</project> </project>

View File

@ -3,10 +3,11 @@
<groupId>org.ciyam</groupId> <groupId>org.ciyam</groupId>
<artifactId>AT</artifactId> <artifactId>AT</artifactId>
<versioning> <versioning>
<release>1.3.4</release> <release>1.3.5</release>
<versions> <versions>
<version>1.3.4</version> <version>1.3.4</version>
<version>1.3.5</version>
</versions> </versions>
<lastUpdated>20200414162728</lastUpdated> <lastUpdated>20200717104214</lastUpdated>
</versioning> </versioning>
</metadata> </metadata>

View File

@ -9,7 +9,7 @@
<bitcoinj.version>0.15.5</bitcoinj.version> <bitcoinj.version>0.15.5</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version> <bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp> <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-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version> <commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version> <dagger.version>1.2.2</dagger.version>

View File

@ -43,6 +43,8 @@ import org.qortal.api.websocket.ActiveChatsWebSocket;
import org.qortal.api.websocket.AdminStatusWebSocket; import org.qortal.api.websocket.AdminStatusWebSocket;
import org.qortal.api.websocket.BlocksWebSocket; import org.qortal.api.websocket.BlocksWebSocket;
import org.qortal.api.websocket.ChatMessagesWebSocket; import org.qortal.api.websocket.ChatMessagesWebSocket;
import org.qortal.api.websocket.TradeBotWebSocket;
import org.qortal.api.websocket.TradeOffersWebSocket;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
public class ApiService { public class ApiService {
@ -196,6 +198,8 @@ public class ApiService {
context.addServlet(BlocksWebSocket.class, "/websockets/blocks"); context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*"); context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages"); context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
// Start server // Start server
this.server.start(); this.server.start();

View File

@ -25,6 +25,9 @@ public class CrossChainBitcoinRedeemRequest {
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG") @Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
public byte[] secret; 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() { public CrossChainBitcoinRedeemRequest() {
} }

View File

@ -12,20 +12,19 @@ public class CrossChainBuildRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey; 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") @Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @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") @Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount; public long fundingQortAmount;
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] bitcoinPublicKeyHash;
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV") @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") @Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)

View File

@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest { public class CrossChainCancelRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey; public byte[] creatorPublicKey;
@Schema(description = "Qortal AT address") @Schema(description = "Qortal trade AT address")
public String atAddress; public String atAddress;
public CrossChainCancelRequest() { public CrossChainCancelRequest() {

View File

@ -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;
}
}

View File

@ -8,14 +8,20 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest { public class CrossChainSecretRequest {
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] recipientPublicKey; public byte[] partnerPublicKey;
@Schema(description = "Qortal AT address") @Schema(description = "Qortal AT address")
public String atAddress; public String atAddress;
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG") @Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
public byte[] secret; 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() { public CrossChainSecretRequest() {
} }

View File

@ -8,14 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeRequest { public class CrossChainTradeRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") @Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey; public byte[] tradePublicKey;
@Schema(description = "Qortal AT address") @Schema(description = "Qortal AT address")
public String atAddress; public String atAddress;
@Schema(description = "Qortal address for trade partner/recipient") @Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
public String recipient; public byte[] messageTransactionSignature;
public CrossChainTradeRequest() { public CrossChainTradeRequest() {
} }

View File

@ -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;
}
}

View File

@ -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() {
}
}

View File

@ -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

View File

@ -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.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier; import org.qortal.controller.ChatNotifier;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
@ -24,7 +23,7 @@ import org.qortal.repository.RepositoryManager;
@WebSocket @WebSocket
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSocket { public class ActiveChatsWebSocket extends ApiWebSocket {
@Override @Override
public void configure(WebSocketServletFactory factory) { public void configure(WebSocketServletFactory factory) {
@ -33,7 +32,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
@OnWebSocketConnect @OnWebSocketConnect
public void onWebSocketConnect(Session session) { 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"); String address = pathParams.get("address");
if (address == null || !Crypto.isValidAddress(address)) { if (address == null || !Crypto.isValidAddress(address)) {
@ -76,7 +75,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
this.marshall(stringWriter, activeChats); marshall(stringWriter, activeChats);
// Only output if something has changed // Only output if something has changed
String output = stringWriter.toString(); String output = stringWriter.toString();

View File

@ -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.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.NodeStatus; import org.qortal.api.model.NodeStatus;
import org.qortal.controller.StatusNotifier; import org.qortal.controller.StatusNotifier;
@ -21,7 +20,7 @@ import org.qortal.repository.RepositoryManager;
@WebSocket @WebSocket
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSocket { public class AdminStatusWebSocket extends ApiWebSocket {
@Override @Override
public void configure(WebSocketServletFactory factory) { public void configure(WebSocketServletFactory factory) {
@ -57,7 +56,7 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
this.marshall(stringWriter, nodeStatus); marshall(stringWriter, nodeStatus);
// Only output if something has changed // Only output if something has changed
String output = stringWriter.toString(); String output = stringWriter.toString();

View File

@ -3,7 +3,10 @@ package org.qortal.api.websocket;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.xml.bind.JAXBContext; 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.http.pathmap.UriTemplatePathSpec;
import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; 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.JAXBContextFactory;
import org.eclipse.persistence.jaxb.MarshallerProperties; import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.qortal.api.ApiError; import org.qortal.api.ApiError;
import org.qortal.api.ApiErrorRoot; 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(); ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
return upgradeRequest.getHttpServletRequest().getPathInfo(); 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); 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 apiErrorRoot = new ApiErrorRoot();
apiErrorRoot.setApiError(apiError); 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()); Marshaller marshaller = createMarshaller(object.getClass());
try { 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 is empty then we're returning "[]" anyway
if (collection.isEmpty()) { if (collection.isEmpty()) {
writer.append("[]"); 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()));
}
}
} }

View File

@ -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.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError; import org.qortal.api.ApiError;
import org.qortal.api.model.BlockInfo; import org.qortal.api.model.BlockInfo;
@ -23,7 +22,7 @@ import org.qortal.utils.Base58;
@WebSocket @WebSocket
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket { public class BlocksWebSocket extends ApiWebSocket {
@Override @Override
public void configure(WebSocketServletFactory factory) { public void configure(WebSocketServletFactory factory) {
@ -111,7 +110,7 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
try { try {
this.marshall(stringWriter, blockInfo); marshall(stringWriter, blockInfo);
session.getRemote().sendStringByFuture(stringWriter.toString()); session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) { } catch (IOException | WebSocketException e) {

View File

@ -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.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier; import org.qortal.controller.ChatNotifier;
import org.qortal.data.chat.ChatMessage; import org.qortal.data.chat.ChatMessage;
@ -25,7 +24,7 @@ import org.qortal.repository.RepositoryManager;
@WebSocket @WebSocket
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSocket { public class ChatMessagesWebSocket extends ApiWebSocket {
@Override @Override
public void configure(WebSocketServletFactory factory) { public void configure(WebSocketServletFactory factory) {
@ -129,7 +128,7 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
try { try {
this.marshall(stringWriter, chatMessages); marshall(stringWriter, chatMessages);
session.getRemote().sendStringByFuture(stringWriter.toString()); session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) { } catch (IOException | WebSocketException e) {

View 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;
}
}

View 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;
}
}

View File

@ -17,7 +17,6 @@ import org.qortal.account.Account;
import org.qortal.account.NullAccount; import org.qortal.account.NullAccount;
import org.qortal.account.PublicKeyAccount; import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.CiyamAtSettings; import org.qortal.block.BlockChain.CiyamAtSettings;
import org.qortal.crypto.Crypto; 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.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group; import org.qortal.group.Group;
import org.qortal.repository.BlockRepository; import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction; import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58; import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
@ -133,9 +132,9 @@ public class QortalATAPI extends API {
byte[] signature = blockSummaries.get(0).getSignature(); 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. // 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.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
this.setA3(state, fromBytes(signature, 60)); this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
this.setA4(state, fromBytes(signature, 68)); this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
} catch (DataException e) { } catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", 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 height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1; int sequence = timestamp.transactionSequence + 1;
BlockRepository blockRepository = this.getRepository().getBlockRepository(); ATRepository.NextTransactionInfo nextTransactionInfo;
try { try {
int currentHeight = blockRepository.getBlockchainHeight(); nextTransactionInfo = this.getRepository().getATRepository().findNextTransaction(atAddress, height, sequence);
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);
} catch (DataException e) { } catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", 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 @Override
@ -282,7 +249,7 @@ public class QortalATAPI extends API {
byte[] hash = Crypto.digest(input); byte[] hash = Crypto.digest(input);
return fromBytes(hash, 0); return BitTwiddling.longFromBEBytes(hash, 0);
} catch (DataException e) { } catch (DataException e) {
throw new RuntimeException("AT API unable to fetch latest block from repository?", 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); TransactionData transactionData = this.getTransactionFromA(state);
byte[] messageData = null; byte[] messageData = this.getMessageFromTransaction(transactionData);
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;
// Pad messageData to fit B // Pad messageData to fit B
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0); if (messageData.length < 4 * 8)
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally // Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
this.setB(state, paddedMessageData); this.setB(state, messageData);
} }
@Override @Override
@ -457,12 +408,6 @@ public class QortalATAPI extends API {
// Utility methods // 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. */ /** 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) { public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32); 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 // Compare end of transaction's signature against A2 thru A4
byte[] sig = transactionData.getSignature(); 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"); 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 */ /** Returns AT's account */
/* package */ Account getATAccount() { /* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress()); return new Account(this.repository, this.atData.getATAddress());
@ -563,4 +522,8 @@ public class QortalATAPI extends API {
super.setB(state, bBytes); super.setB(state, bBytes);
} }
protected void zeroB(MachineState state) {
super.zeroB(state);
}
} }

View File

@ -12,6 +12,7 @@ import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState; import org.ciyam.at.MachineState;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings; import org.qortal.settings.Settings;
/** /**
@ -22,8 +23,70 @@ import org.qortal.settings.Settings;
*/ */
public enum QortalFunctionCode { public enum QortalFunctionCode {
/** /**
* <tt>0x0510</tt><br> * Returns length of message data from transaction in A.<br>
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3. * <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) { CONVERT_B_TO_PKH(0x0510, 0, false) {
@Override @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> * 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. * P2SH stored in lower 25 bytes of B.
*/ */
CONVERT_B_TO_P2SH(0x0511, 0, false) { 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> * 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. * Qortal address stored in lower 25 bytes of B.
*/ */
CONVERT_B_TO_QORTAL(0x0512, 0, false) { CONVERT_B_TO_QORTAL(0x0512, 0, false) {

View File

@ -792,6 +792,9 @@ public class Controller extends Thread {
this.notifyGroupMembershipChange = false; this.notifyGroupMembershipChange = false;
ChatNotifier.getInstance().onGroupMembershipChange(); ChatNotifier.getInstance().onGroupMembershipChange();
} }
// Trade-bot might want to perform some actions too
TradeBot.getInstance().onChainTipChange();
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,45 @@
package org.qortal.crosschain; package org.qortal.crosschain;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address; import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin; 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.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput; 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.MainNetParams;
import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.MonetaryFormat; 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.settings.Settings;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode;
public class BTC { 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 NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1; public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20; public static final int HASH160_LENGTH = 20;
@ -30,6 +47,7 @@ public class BTC {
protected static final Logger LOGGER = LogManager.getLogger(BTC.class); protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32; private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
public enum BitcoinNet { public enum BitcoinNet {
MAIN { MAIN {
@ -58,6 +76,9 @@ public class BTC {
private final NetworkParameters params; private final NetworkParameters params;
private final ElectrumX electrumX; private final ElectrumX electrumX;
// Let ECKey.equals() do the hard work
private final Set<ECKey> spentKeys = new HashSet<>();
// Constructors and instance // Constructors and instance
private BTC() { private BTC() {
@ -88,6 +109,34 @@ public class BTC {
// Actual useful methods for use by other classes // 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. */ /** Returns median timestamp from latest 11 blocks, in seconds. */
public Integer getMedianBlockTime() { public Integer getMedianBlockTime() {
Integer height = this.electrumX.getCurrentHeight(); Integer height = this.electrumX.getCurrentHeight();
@ -99,34 +148,31 @@ public class BTC {
if (blockHeaders == null || blockHeaders.size() < 11) if (blockHeaders == null || blockHeaders.size() < 11)
return null; 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)); blockTimestamps.sort((a, b) -> Integer.compare(b, a));
// Pick median
return blockTimestamps.get(5); return blockTimestamps.get(5);
} }
public Coin getBalance(String base58Address) { public Long getBalance(String base58Address) {
Long balance = this.electrumX.getBalance(addressToScript(base58Address)); return this.electrumX.getBalance(addressToScript(base58Address));
if (balance == null)
return null;
return Coin.valueOf(balance);
} }
public List<TransactionOutput> getUnspentOutputs(String 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) if (unspentOutputs == null)
return null; return null;
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>(); List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) { for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA()); List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
if (transactionOutputs == null) if (transactionOutputs == null)
return null; return null;
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB())); unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
} }
return unspentTransactionOutputs; return unspentTransactionOutputs;
@ -141,6 +187,7 @@ public class BTC {
return transaction.getOutputs(); return transaction.getOutputs();
} }
/** Returns list of raw transactions spending passed address. */
public List<byte[]> getAddressTransactions(String base58Address) { public List<byte[]> getAddressTransactions(String base58Address) {
return this.electrumX.getAddressTransactions(addressToScript(base58Address)); return this.electrumX.getAddressTransactions(addressToScript(base58Address));
} }
@ -149,6 +196,181 @@ public class BTC {
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize()); 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 // Utility methods for us
private byte[] addressToScript(String base58Address) { private byte[] addressToScript(String base58Address) {

File diff suppressed because it is too large Load Diff

View 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;
}
}

View File

@ -25,11 +25,11 @@ import org.json.simple.JSONObject;
import org.json.simple.JSONValue; import org.json.simple.JSONValue;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.crypto.TrustlessSSLSocketFactory; import org.qortal.crypto.TrustlessSSLSocketFactory;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */
public class ElectrumX { public class ElectrumX {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class); private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
@ -93,7 +93,21 @@ public class ElectrumX {
private ElectrumX(String bitcoinNetwork) { private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) { switch (bitcoinNetwork) {
case "MAIN": 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; break;
case "TEST3": case "TEST3":
@ -119,6 +133,7 @@ public class ElectrumX {
rpc("server.banner"); rpc("server.banner");
} }
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
public static synchronized ElectrumX getInstance(String bitcoinNetwork) { public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
if (!instances.containsKey(bitcoinNetwork)) if (!instances.containsKey(bitcoinNetwork))
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork)); instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
@ -129,16 +144,26 @@ public class ElectrumX {
// Methods for use by other classes // Methods for use by other classes
public Integer getCurrentHeight() { public Integer getCurrentHeight() {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe"); Object blockObj = this.rpc("blockchain.headers.subscribe");
if (blockJson == null || !blockJson.containsKey("height")) if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("height"))
return null; return null;
return ((Long) blockJson.get("height")).intValue(); return ((Long) blockJson.get("height")).intValue();
} }
public List<byte[]> getBlockHeaders(int startHeight, long count) { public List<byte[]> getBlockHeaders(int startHeight, long count) {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.block.headers", startHeight, count); Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (blockJson == null || !blockJson.containsKey("count") || !blockJson.containsKey("hex")) if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null; return null;
Long returnedCount = (Long) blockJson.get("count"); Long returnedCount = (Long) blockJson.get("count");
@ -155,57 +180,87 @@ public class ElectrumX {
return rawBlockHeaders; 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) { public Long getBalance(byte[] script) {
byte[] scriptHash = Crypto.digest(script); byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash); Bytes.reverse(scriptHash);
JSONObject balanceJson = (JSONObject) this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString()); Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (balanceJson == null || !balanceJson.containsKey("confirmed")) if (!(balanceObj instanceof JSONObject))
return null;
JSONObject balanceJson = (JSONObject) balanceObj;
if (!balanceJson.containsKey("confirmed"))
return null; return null;
return (Long) balanceJson.get("confirmed"); 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); byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash); Bytes.reverse(scriptHash);
JSONArray unspentJson = (JSONArray) this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString()); Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (unspentJson == null) if (!(unspentJson instanceof JSONArray))
return null; return null;
List<Pair<byte[], Integer>> unspentOutputs = new ArrayList<>(); List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : unspentJson) { for (Object rawUnspent : (JSONArray) unspentJson) {
JSONObject unspent = (JSONObject) rawUnspent; 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(); byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
int outputIndex = ((Long) unspent.get("tx_pos")).intValue(); 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; return unspentOutputs;
} }
/** Returns raw transaction for passed transaction hash, or null if not found. */
public byte[] getRawTransaction(byte[] txHash) { public byte[] getRawTransaction(byte[] txHash) {
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString()); Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (rawTransactionHex == null) if (!(rawTransactionHex instanceof String))
return null; 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) { public List<byte[]> getAddressTransactions(byte[] script) {
byte[] scriptHash = Crypto.digest(script); byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash); Bytes.reverse(scriptHash);
JSONArray transactionsJson = (JSONArray) this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString()); Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (transactionsJson == null) if (!(transactionsJson instanceof JSONArray))
return null; return null;
List<byte[]> rawTransactions = new ArrayList<>(); List<byte[]> rawTransactions = new ArrayList<>();
for (Object rawTransactionInfo : transactionsJson) { for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo; JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions // We only want confirmed transactions
@ -223,6 +278,7 @@ public class ElectrumX {
return rawTransactions; return rawTransactions;
} }
/** Returns true if raw transaction successfully broadcast. */
public boolean broadcastTransaction(byte[] transactionBytes) { public boolean broadcastTransaction(byte[] transactionBytes) {
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString()); Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
if (rawBroadcastResult == null) if (rawBroadcastResult == null)
@ -235,14 +291,15 @@ public class ElectrumX {
// Class-private utility methods // Class-private utility methods
/** Query current server for its list of peer servers, and return those we can parse. */
private Set<Server> serverPeersSubscribe() { private Set<Server> serverPeersSubscribe() {
Set<Server> newServers = new HashSet<>(); Set<Server> newServers = new HashSet<>();
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe"); Object peers = this.connectedRpc("server.peers.subscribe");
if (peers == null) if (!(peers instanceof JSONArray))
return newServers; return newServers;
for (Object rawPeer : peers) { for (Object rawPeer : (JSONArray) peers) {
JSONArray peer = (JSONArray) rawPeer; JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3) if (peer.size() < 3)
continue; continue;
@ -287,6 +344,7 @@ public class ElectrumX {
return newServers; return newServers;
} }
/** Return output from RPC call, with automatic reconnection to different server if needed. */
private synchronized Object rpc(String method, Object...params) { private synchronized Object rpc(String method, Object...params) {
while (haveConnection()) { while (haveConnection()) {
Object response = connectedRpc(method, params); Object response = connectedRpc(method, params);
@ -305,6 +363,7 @@ public class ElectrumX {
return null; return null;
} }
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() { private boolean haveConnection() {
if (this.currentServer != null) if (this.currentServer != null)
return true; return true;
@ -377,10 +436,12 @@ public class ElectrumX {
if (response.isEmpty()) if (response.isEmpty())
return null; return null;
JSONObject responseJson = (JSONObject) JSONValue.parse(response); Object responseObj = JSONValue.parse(response);
if (responseJson == null) if (!(responseObj instanceof JSONObject))
return null; return null;
JSONObject responseJson = (JSONObject) responseObj;
return responseJson.get("result"); return responseJson.get("result");
} }

View File

@ -4,14 +4,14 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTCACCT;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB // All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeData { public class CrossChainTradeData {
public enum Mode { OFFER, TRADE };
// Properties // Properties
@Schema(description = "AT's Qortal address") @Schema(description = "AT's Qortal address")
@ -20,32 +20,40 @@ public class CrossChainTradeData {
@Schema(description = "AT creator's Qortal address") @Schema(description = "AT creator's Qortal address")
public String qortalCreator; 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)") @Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
public long creationTimestamp; public long creationTimestamp;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
public int tradeTimeout;
@Schema(description = "AT's current QORT balance") @Schema(description = "AT's current QORT balance")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortBalance; public long qortBalance;
@Schema(description = "HASH160 of 32-byte secret") @Schema(description = "HASH160 of 32-byte secret-A")
public byte[] secretHash; public byte[] hashOfSecretA;
@Schema(description = "Initial QORT payment that will be sent to Qortal trade partner") @Schema(description = "HASH160 of 32-byte secret-B")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) public byte[] hashOfSecretB;
public long initialPayout;
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner") @Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @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)") @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") @Schema(description = "Timestamp when AT switched to trade mode")
public Long tradeModeTimestamp; public Long tradeModeTimestamp;
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)") @Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
public long tradeRefundTimeout; public Integer refundTimeout;
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)") @Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
public Integer tradeRefundHeight; public Integer tradeRefundHeight;
@ -54,10 +62,19 @@ public class CrossChainTradeData {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long expectedBitcoin; public long expectedBitcoin;
public Mode mode; public BTCACCT.Mode mode;
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout") @Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
public Integer lockTime; public Integer lockTimeA;
@Schema(description = "Suggested Bitcoin P2SH-B nLockTime based on trade timeout")
public Integer lockTimeB;
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
public byte[] partnerBitcoinPKH;
@Schema(description = "Trade partner's Qortal receiving address")
public String qortalPartnerReceivingAddress;
// Constructors // Constructors

View 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;
}
}

View File

@ -0,0 +1,5 @@
package org.qortal.event;
public interface Event {
}

View 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);
}
}

View File

@ -0,0 +1,6 @@
package org.qortal.event;
@FunctionalInterface
public interface Listener {
void listen(Event event);
}

View File

@ -15,6 +15,9 @@ public interface ATRepository {
/** Returns where AT with passed address exists in repository */ /** Returns where AT with passed address exists in repository */
public boolean exists(String atAddress) throws DataException; 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 */ /** Returns list of executable ATs, empty if none found */
public List<ATData> getAllExecutableATs() throws DataException; public List<ATData> getAllExecutableATs() throws DataException;
@ -54,6 +57,24 @@ public interface ATRepository {
*/ */
public ATStateData getLatestATState(String atAddress) throws DataException; 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. * Returns all ATStateData for a given block height.
* <p> * <p>
@ -88,4 +109,28 @@ public interface ATRepository {
/** Delete state data for all ATs at this height */ /** Delete state data for all ATs at this height */
public void deleteATStates(int height) throws DataException; 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;
} }

View File

@ -61,6 +61,15 @@ public interface BlockRepository {
*/ */
public int getHeightFromTimestamp(long timestamp) throws DataException; 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. * Return highest block height from repository.
* *

View File

@ -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;
}

View File

@ -14,6 +14,8 @@ public interface Repository extends AutoCloseable {
public ChatRepository getChatRepository(); public ChatRepository getChatRepository();
public CrossChainRepository getCrossChainRepository();
public GroupRepository getGroupRepository(); public GroupRepository getGroupRepository();
public NameRepository getNameRepository(); public NameRepository getNameRepository();

View File

@ -6,6 +6,7 @@ import java.util.Map;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.group.GroupApprovalData; import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.GroupApprovalTransactionData; import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData; import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.TransactionType;
@ -107,6 +108,18 @@ public interface TransactionRepository {
*/ */
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException; 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. * Returns list of transactions relating to specific asset ID.
* *

View File

@ -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 @Override
public List<ATData> getAllExecutableATs() throws DataException { public List<ATData> getAllExecutableATs() throws DataException {
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, " 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 @Override
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException { public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
String sql = "SELECT AT_address, state_hash, fees, is_initial " 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);
}
}
} }

View File

@ -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 @Override
public int getBlockchainHeight() throws DataException { public int getBlockchainHeight() throws DataException {
String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1"; String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1";

View File

@ -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);
}
}
}

View File

@ -4,9 +4,14 @@ import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.qortal.utils.Base58;
import com.google.common.hash.HashCode;
public class HSQLDBDatabaseUpdates { public class HSQLDBDatabaseUpdates {
@ -618,6 +623,22 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")"); stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")");
break; 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: default:
// nothing to do // nothing to do
return false; return false;

View File

@ -32,6 +32,7 @@ import org.qortal.repository.ArbitraryRepository;
import org.qortal.repository.AssetRepository; import org.qortal.repository.AssetRepository;
import org.qortal.repository.BlockRepository; import org.qortal.repository.BlockRepository;
import org.qortal.repository.ChatRepository; import org.qortal.repository.ChatRepository;
import org.qortal.repository.CrossChainRepository;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository; import org.qortal.repository.GroupRepository;
import org.qortal.repository.NameRepository; import org.qortal.repository.NameRepository;
@ -115,6 +116,11 @@ public class HSQLDBRepository implements Repository {
return new HSQLDBChatRepository(this); return new HSQLDBChatRepository(this);
} }
@Override
public CrossChainRepository getCrossChainRepository() {
return new HSQLDBCrossChainRepository(this);
}
@Override @Override
public GroupRepository getGroupRepository() { public GroupRepository getGroupRepository() {
return new HSQLDBGroupRepository(this); return new HSQLDBGroupRepository(this);

View File

@ -19,6 +19,7 @@ import org.qortal.data.PaymentData;
import org.qortal.data.group.GroupApprovalData; import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.GroupApprovalTransactionData; import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData; import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.repository.DataException; 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 @Override
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
throws DataException { throws DataException {

View File

@ -26,7 +26,7 @@ import com.google.common.base.Utf8;
public class DeployAtTransaction extends Transaction { public class DeployAtTransaction extends Transaction {
// Properties // Properties
private DeployAtTransactionData deployATTransactionData; private DeployAtTransactionData deployAtTransactionData;
// Other useful constants // Other useful constants
public static final int MAX_NAME_SIZE = 200; public static final int MAX_NAME_SIZE = 200;
@ -40,31 +40,31 @@ public class DeployAtTransaction extends Transaction {
public DeployAtTransaction(Repository repository, TransactionData transactionData) { public DeployAtTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData); super(repository, transactionData);
this.deployATTransactionData = (DeployAtTransactionData) this.transactionData; this.deployAtTransactionData = (DeployAtTransactionData) this.transactionData;
} }
// More information // More information
@Override @Override
public List<String> getRecipientAddresses() throws DataException { 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 */ /** Returns AT version from the header bytes */
private short getVersion() { private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes(); byte[] creationBytes = deployAtTransactionData.getCreationBytes();
return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian
} }
/** Make sure deployATTransactionData has an ATAddress */ /** Make sure deployATTransactionData has an ATAddress */
private void ensureATAddress() throws DataException { public static void ensureATAddress(DeployAtTransactionData deployAtTransactionData) throws DataException {
if (this.deployATTransactionData.getAtAddress() != null) if (deployAtTransactionData.getAtAddress() != null)
return; return;
// Use transaction transformer // Use transaction transformer
try { try {
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(this.deployATTransactionData)); String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(deployAtTransactionData));
this.deployATTransactionData.setAtAddress(atAddress); deployAtTransactionData.setAtAddress(atAddress);
} catch (TransformationException e) { } catch (TransformationException e) {
throw new DataException("Unable to generate AT address"); throw new DataException("Unable to generate AT address");
} }
@ -73,9 +73,9 @@ public class DeployAtTransaction extends Transaction {
// Navigation // Navigation
public Account getATAccount() throws DataException { 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 // Processing
@ -83,30 +83,30 @@ public class DeployAtTransaction extends Transaction {
@Override @Override
public ValidationResult isValid() throws DataException { public ValidationResult isValid() throws DataException {
// Check name size bounds // 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) if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH; return ValidationResult.INVALID_NAME_LENGTH;
// Check description size bounds // 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) if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
return ValidationResult.INVALID_DESCRIPTION_LENGTH; return ValidationResult.INVALID_DESCRIPTION_LENGTH;
// Check AT-type size bounds // 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) if (atTypeLength < 1 || atTypeLength > MAX_AT_TYPE_SIZE)
return ValidationResult.INVALID_AT_TYPE_LENGTH; return ValidationResult.INVALID_AT_TYPE_LENGTH;
// Check tags size bounds // 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) if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
return ValidationResult.INVALID_TAGS_LENGTH; return ValidationResult.INVALID_TAGS_LENGTH;
// Check amount is positive // Check amount is positive
if (this.deployATTransactionData.getAmount() <= 0) if (this.deployAtTransactionData.getAmount() <= 0)
return ValidationResult.NEGATIVE_AMOUNT; return ValidationResult.NEGATIVE_AMOUNT;
long assetId = this.deployATTransactionData.getAssetId(); long assetId = this.deployAtTransactionData.getAssetId();
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId); AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
// Check asset even exists // Check asset even exists
if (assetData == null) if (assetData == null)
@ -117,7 +117,7 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.ASSET_NOT_SPENDABLE; return ValidationResult.ASSET_NOT_SPENDABLE;
// Check asset amount is integer if asset is not divisible // 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; return ValidationResult.INVALID_AMOUNT;
Account creator = this.getCreator(); Account creator = this.getCreator();
@ -125,15 +125,15 @@ public class DeployAtTransaction extends Transaction {
// Check creator has enough funds // Check creator has enough funds
if (assetId == Asset.QORT) { if (assetId == Asset.QORT) {
// Simple case: amount and fee both in 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) if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
} else { } else {
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee()) if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount()) if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
} }
@ -142,12 +142,12 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.INVALID_CREATION_BYTES; return ValidationResult.INVALID_CREATION_BYTES;
// Check creation bytes are valid (for v2+) // Check creation bytes are valid (for v2+)
this.ensureATAddress(); ensureATAddress(this.deployAtTransactionData);
// Just enough AT data to allow API to query initial balances, etc. // Just enough AT data to allow API to query initial balances, etc.
String atAddress = this.deployATTransactionData.getAtAddress(); String atAddress = this.deployAtTransactionData.getAtAddress();
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey(); byte[] creatorPublicKey = this.deployAtTransactionData.getCreatorPublicKey();
long creation = this.deployATTransactionData.getTimestamp(); long creation = this.deployAtTransactionData.getTimestamp();
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId); ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1; int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
@ -157,7 +157,7 @@ public class DeployAtTransaction extends Transaction {
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
try { try {
new MachineState(api, loggerFactory, this.deployATTransactionData.getCreationBytes()); new MachineState(api, loggerFactory, this.deployAtTransactionData.getCreationBytes());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// Not valid // Not valid
return ValidationResult.INVALID_CREATION_BYTES; return ValidationResult.INVALID_CREATION_BYTES;
@ -169,25 +169,25 @@ public class DeployAtTransaction extends Transaction {
@Override @Override
public ValidationResult isProcessable() throws DataException { public ValidationResult isProcessable() throws DataException {
Account creator = getCreator(); Account creator = getCreator();
long assetId = this.deployATTransactionData.getAssetId(); long assetId = this.deployAtTransactionData.getAssetId();
// Check creator has enough funds // Check creator has enough funds
if (assetId == Asset.QORT) { if (assetId == Asset.QORT) {
// Simple case: amount and fee both in 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) if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
} else { } else {
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee()) if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount()) if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
return ValidationResult.NO_BALANCE; return ValidationResult.NO_BALANCE;
} }
// Check AT doesn't already exist // 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.AT_ALREADY_EXISTS;
return ValidationResult.OK; return ValidationResult.OK;
@ -195,40 +195,40 @@ public class DeployAtTransaction extends Transaction {
@Override @Override
public void process() throws DataException { public void process() throws DataException {
this.ensureATAddress(); ensureATAddress(this.deployAtTransactionData);
// Deploy AT, saving into repository // Deploy AT, saving into repository
AT at = new AT(this.repository, this.deployATTransactionData); AT at = new AT(this.repository, this.deployAtTransactionData);
at.deploy(); at.deploy();
long assetId = this.deployATTransactionData.getAssetId(); long assetId = this.deployAtTransactionData.getAssetId();
// Update creator's balance regarding initial payment to AT // Update creator's balance regarding initial payment to AT
Account creator = getCreator(); Account creator = getCreator();
creator.modifyAssetBalance(assetId, - this.deployATTransactionData.getAmount()); creator.modifyAssetBalance(assetId, - this.deployAtTransactionData.getAmount());
// Update AT's reference, which also creates AT account // Update AT's reference, which also creates AT account
Account atAccount = this.getATAccount(); Account atAccount = this.getATAccount();
atAccount.setLastReference(this.deployATTransactionData.getSignature()); atAccount.setLastReference(this.deployAtTransactionData.getSignature());
// Update AT's balance // Update AT's balance
atAccount.setConfirmedBalance(assetId, this.deployATTransactionData.getAmount()); atAccount.setConfirmedBalance(assetId, this.deployAtTransactionData.getAmount());
} }
@Override @Override
public void orphan() throws DataException { public void orphan() throws DataException {
// Delete AT from repository // Delete AT from repository
AT at = new AT(this.repository, this.deployATTransactionData); AT at = new AT(this.repository, this.deployAtTransactionData);
at.undeploy(); at.undeploy();
long assetId = this.deployATTransactionData.getAssetId(); long assetId = this.deployAtTransactionData.getAssetId();
// Update creator's balance regarding initial payment to AT // Update creator's balance regarding initial payment to AT
Account creator = getCreator(); Account creator = getCreator();
creator.modifyAssetBalance(assetId, this.deployATTransactionData.getAmount()); creator.modifyAssetBalance(assetId, this.deployAtTransactionData.getAmount());
// Delete AT's account (and hence its balance) // Delete AT's account (and hence its balance)
this.repository.getAccountRepository().delete(this.deployATTransactionData.getAtAddress()); this.repository.getAccountRepository().delete(this.deployAtTransactionData.getAtAddress());
} }
} }

View File

@ -26,9 +26,26 @@ public class BitTwiddling {
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) }; 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 */ /** 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; 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);
}
} }

View 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;
}
}

View 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);
}
}

View 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;
}
}

View File

@ -1,7 +1,6 @@
package org.qortal.test.btcacct; package org.qortal.test.btcacct;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertTrue;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -10,14 +9,15 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.function.Function; import java.util.function.Function;
import org.bitcoinj.core.Base58;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
@ -43,14 +43,18 @@ import com.google.common.primitives.Bytes;
public class AtTests extends Common { public class AtTests extends Common {
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes(); public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final int refundTimeout = 10; // blocks public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
public static final long initialPayout = 100000L; 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 redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L; public static final long fundingAmount = 123_45600000L;
public static final long bitcoinAmount = 864200L; public static final long bitcoinAmount = 864200L;
private static final Random RANDOM = new Random();
@Before @Before
public void beforeTest() throws DataException { public void beforeTest() throws DataException {
Common.useDefaultSettings(); Common.useDefaultSettings();
@ -58,9 +62,9 @@ public class AtTests extends Common {
@Test @Test
public void testCompile() { 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()); 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 { public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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 expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@ -85,10 +91,10 @@ public class AtTests extends Common {
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance); assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance; expectedBalance = partnersInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT); 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 // Test orphaning
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
@ -103,10 +109,10 @@ public class AtTests extends Common {
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance; expectedBalance = partnersInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT); 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 { public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee(); long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT // Send creator's address to AT, instead of typical partner's address
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0); byte[] messageData = BTCACCT.buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee(); 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); 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 expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee; 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 above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance); assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
describeAt(repository, atAddress);
// Test orphaning // Test orphaning
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee; long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT); actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@ -157,71 +175,144 @@ public class AtTests extends Common {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Test @Test
public void testInitialPayment() throws DataException { public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
// Send recipient's address to AT long deployAtFee = deployAtTransaction.getTransactionData().getFee();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
// 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); 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); describeAt(repository, atAddress);
// Test orphaning // Check AT is finished
BlockUtils.orphanLastBlock(repository); ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
expectedBalance = recipientsInitialBalance; // AT should be in CANCELLED mode
actualBalance = recipient.getConfirmedBalance(Asset.QORT); CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
} }
} }
// 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") @SuppressWarnings("unused")
@Test @Test
public void testIncorrectTradeSender() throws DataException { public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
// Send recipient's address to AT BUT NOT FROM AT CREATOR long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress); 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); BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance; long expectedBalance = partnersInitialBalance;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT); 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); 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 { public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
// Send recipient's address to AT long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Initial payment should happen 1st block after receiving recipient address // Send trade info to AT
BlockUtils.mintBlock(repository); 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 deployAtFee = deployAtTransaction.getTransactionData().getFee();
long messageFee = messageTransaction.getTransactionData().getFee(); long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress); describeAt(repository, atAddress);
// Test orphaning // Check AT is finished
BlockUtils.orphanLastBlock(repository); ATData atData = repository.getATRepository().fromATAddress(atAddress);
BlockUtils.orphanLastBlock(repository); 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 expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@ -267,46 +372,63 @@ public class AtTests extends Common {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Test @Test
public void testCorrectSecretCorrectSender() throws DataException { public void testCorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
// Send recipient's address to AT long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); 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); BlockUtils.mintBlock(repository);
// Send correct secret to AT // Send correct secrets to AT, from correct account
messageTransaction = sendMessage(repository, recipient, secret, atAddress); messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block // AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
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); 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 // Orphan redeem
BlockUtils.orphanLastBlock(repository); BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee(); // Check balances
actualBalance = recipient.getConfirmedBalance(Asset.QORT); 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 // Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
@ -317,99 +439,206 @@ public class AtTests extends Common {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Test @Test
public void testCorrectSecretIncorrectSender() throws DataException { public void testCorrectSecretsIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
// Send recipient's address to AT long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); 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); BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account // Send correct secrets to AT, but from wrong account
messageTransaction = sendMessage(repository, bystander, secret, atAddress); messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress); 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); checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
} }
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Test @Test
public void testIncorrectSecretCorrectSender() throws DataException { public void testIncorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount(); Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress(); String atAddress = at.getAddress();
// Send recipient's address to AT long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); 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); BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account // Send incorrect secrets to AT, from correct account
byte[] wrongSecret = Crypto.digest(secret); byte[] wrongSecret = new byte[32];
messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress); RANDOM.nextBytes(wrongSecret);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block // AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository); BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee(); describeAt(repository, atAddress);
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
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); 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); 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") @SuppressWarnings("unused")
@Test @Test
public void testDescribeDeployed() throws DataException { public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); 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(); List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
@ -433,8 +662,12 @@ public class AtTests extends Common {
} }
} }
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { private int calcTestLockTimeA(long messageTimestamp) {
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); 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(); long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference(); 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 { private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks // AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount) for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
@ -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 // 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 expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - initialPayout; long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@ -516,40 +750,43 @@ public class AtTests extends Common {
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n" System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n" + "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n" + "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n" + "\tcurrent balance: %s QORT,\n"
+ "\tHASH160 of secret: %s,\n" + "\tis finished: %b,\n"
+ "\tinitial payout: %s QORT,\n" + "\tHASH160 of secret-B: %s,\n"
+ "\tredeem payout: %s QORT,\n" + "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n" + "\texpected bitcoin: %s BTC,\n"
+ "\ttrade timeout: %d minutes (from trade start),\n"
+ "\tcurrent block height: %d,\n", + "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress, tradeData.qortalAtAddress,
tradeData.mode.name(),
tradeData.qortalCreator, tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp), epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance), Amounts.prettyAmount(tradeData.qortBalance),
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40), atData.getIsFinished(),
Amounts.prettyAmount(tradeData.initialPayout), HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.redeemPayout), Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedBitcoin), Amounts.prettyAmount(tradeData.expectedBitcoin),
tradeData.tradeRefundTimeout,
currentBlockHeight)); currentBlockHeight));
// Are we in 'offer' or 'trade' stage? if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
if (tradeData.tradeRefundHeight == null) { System.out.println(String.format("\trefund height: block %d,\n"
// Offer + "\tHASH160 of secret-A: %s,\n"
System.out.println(String.format("\tstatus: 'offer mode'")); + "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
} else { + "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
// Trade + "\ttrade partner: %s",
System.out.println(String.format("\tstatus: 'trade mode',\n"
+ "\ttrade timeout: block %d,\n"
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
+ "\ttrade recipient: %s",
tradeData.tradeRefundHeight, tradeData.tradeRefundHeight,
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L), HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.qortalRecipient)); 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");
}
} }

View File

@ -5,12 +5,13 @@ import static org.junit.Assert.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.BlockStoreException;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCP2SH;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.test.common.Common; import org.qortal.test.common.Common;
@ -55,11 +56,52 @@ public class BtcTests extends Common {
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress); List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = AtTests.secret; byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions); byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret); assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, 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);
}
} }

View File

@ -13,7 +13,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; 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("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); 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("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("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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -115,7 +115,7 @@ public class BuildP2SH {
// Fund P2SH // Fund P2SH
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", 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"); System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
} catch (DataException e) { } catch (DataException e) {

View File

@ -15,7 +15,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; 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("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress)); 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("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("Hash of secret: %s", HashCode.fromBytes(secretHash)));
System.out.println(String.format("P2SH address: %s", p2shAddress)); 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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(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))); 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 // Check P2SH is funded
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString()); Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) { if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2); 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) // Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); 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" : ""))); System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs) 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()) { if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH")); System.err.println(String.format("Can't redeem spent/unfunded P2SH"));

View File

@ -34,20 +34,20 @@ public class DeployAT {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <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 " System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n" + "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n" + "\t80.4020 \\\n"
+ "\t0.00864200 \\\n" + "\t0.00864200 \\\n"
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t0.0001 \\\n"
+ "\t123.456 \\\n" + "\t123.456 \\\n"
+ "\t10")); + "\t10080"));
System.exit(1); System.exit(1);
} }
public static void main(String[] args) { public static void main(String[] args) {
if (args.length != 8) if (args.length != 7)
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
@ -56,8 +56,8 @@ public class DeployAT {
byte[] refundPrivateKey = null; byte[] refundPrivateKey = null;
long redeemAmount = 0; long redeemAmount = 0;
long expectedBitcoin = 0; long expectedBitcoin = 0;
byte[] bitcoinPublicKeyHash = null;
byte[] secretHash = null; byte[] secretHash = null;
long initialPayout = 0;
long fundingAmount = 0; long fundingAmount = 0;
int tradeTimeout = 0; int tradeTimeout = 0;
@ -75,21 +75,21 @@ public class DeployAT {
if (expectedBitcoin <= 0) if (expectedBitcoin <= 0)
usage("Expected BTC amount must be positive"); 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(); secretHash = HashCode.fromString(args[argIndex++]).asBytes();
if (secretHash.length != 20) if (secretHash.length != 20)
usage("Hash of secret must be 20 bytes"); usage("Hash of secret must be 20 bytes");
initialPayout = Long.parseLong(args[argIndex++]);
if (initialPayout < 0)
usage("Initial QORT payout must be positive");
fundingAmount = Long.parseLong(args[argIndex++]); fundingAmount = Long.parseLong(args[argIndex++]);
if (fundingAmount <= redeemAmount) if (fundingAmount <= redeemAmount)
usage("AT funding amount must be greater than QORT redeem amount"); usage("AT funding amount must be greater than QORT redeem amount");
tradeTimeout = Integer.parseInt(args[argIndex++]); tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 10 || tradeTimeout > 50000) if (tradeTimeout < 60 || tradeTimeout > 50000)
usage("AT trade timeout should be between 10 and 50,000 minutes"); usage("Trade timeout (minutes) must be between 60 and 50000");
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); 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))); System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT // 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()); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis(); long txTimestamp = System.currentTimeMillis();

View File

@ -12,8 +12,8 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.Test; import org.junit.Test;
import org.qortal.crosschain.ElectrumX; import org.qortal.crosschain.ElectrumX;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
@ -61,7 +61,7 @@ public class ElectrumXTests {
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset // Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
int offset = 4 + 32 + 32; 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)); 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"); Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<Pair<byte[], Integer>> unspentOutputs = electrumX.getUnspentOutputs(script); List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
assertNotNull(unspentOutputs); assertNotNull(unspentOutputs);
assertFalse(unspentOutputs.isEmpty()); assertFalse(unspentOutputs.isEmpty());
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) 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.getA()).toString(), unspentOutput.getB())); 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 @Test

View File

@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; 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("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 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)); System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
// New/derived info // New/derived info
@ -121,7 +121,7 @@ public class Redeem {
System.out.println(String.format("P2SH address: %s", p2shAddress)); 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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -147,12 +147,12 @@ public class Redeem {
} }
// Check P2SH is funded // Check P2SH is funded
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString()); Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) { if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2); 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) // Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); 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" : ""))); System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs) 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()) { if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH")); System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
@ -179,10 +179,10 @@ public class Redeem {
for (TransactionOutput fundingOutput : fundingOutputs) for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin redeemAmount = p2shBalance.subtract(bitcoinFee); Coin redeemAmount = Coin.valueOf(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))); 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(); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

View File

@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller; import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
import org.qortal.repository.Repository; 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 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("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("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 // New/derived info
@ -120,7 +120,7 @@ public class Refund {
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); 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))); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -151,12 +151,12 @@ public class Refund {
} }
// Check P2SH is funded // Check P2SH is funded
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString()); Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) { if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2); 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) // Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); 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" : ""))); System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs) 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()) { if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't refund spent/unfunded P2SH")); System.err.println(String.format("Can't refund spent/unfunded P2SH"));
@ -183,10 +183,10 @@ public class Refund {
for (TransactionOutput fundingOutput : fundingOutputs) for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin refundAmount = p2shBalance.subtract(bitcoinFee); Coin refundAmount = Coin.valueOf(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))); 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(); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

View File

@ -20,15 +20,19 @@ public class AccountUtils {
public static final int txGroupId = Group.NO_GROUP; public static final int txGroupId = Group.NO_GROUP;
public static final long fee = 1L * Amounts.MULTIPLIER; public static final long fee = 1L * Amounts.MULTIPLIER;
public static void pay(Repository repository, String sender, String recipient, long amount) throws DataException { public static void pay(Repository repository, String testSenderName, String testRecipientName, long amount) throws DataException {
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, sender); PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, testSenderName);
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient); 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(); byte[] reference = sendingAccount.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1; long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null); 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); TransactionUtils.signAndMint(repository, transactionData, sendingAccount);
} }