mirror of https://github.com/qortal/qortal
Browse Source
Extracted AcctMode from BitcoinACCTv1.Mode as the values are common to both Bitcoin/Litecoin ACCTs. Added test apps for deploy, cancel, trade and redeem of LitecoinACCTv1.pull/29/head
catbref
4 years ago
15 changed files with 2150 additions and 66 deletions
@ -0,0 +1,21 @@
|
||||
package org.qortal.crosschain; |
||||
|
||||
import static java.util.Arrays.stream; |
||||
import static java.util.stream.Collectors.toMap; |
||||
|
||||
import java.util.Map; |
||||
|
||||
public enum AcctMode { |
||||
OFFERING(0), TRADING(1), CANCELLED(2), REFUNDED(3), REDEEMED(4); |
||||
|
||||
public final int value; |
||||
private static final Map<Integer, AcctMode> map = stream(AcctMode.values()).collect(toMap(mode -> mode.value, mode -> mode)); |
||||
|
||||
AcctMode(int value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
public static AcctMode valueOf(int value) { |
||||
return map.get(value); |
||||
} |
||||
} |
@ -0,0 +1,832 @@
|
||||
package org.qortal.crosschain; |
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset; |
||||
|
||||
import java.nio.ByteBuffer; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import org.ciyam.at.API; |
||||
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.qortal.account.Account; |
||||
import org.qortal.asset.Asset; |
||||
import org.qortal.at.QortalFunctionCode; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.data.at.ATData; |
||||
import org.qortal.data.at.ATStateData; |
||||
import org.qortal.data.crosschain.CrossChainTradeData; |
||||
import org.qortal.data.transaction.MessageTransactionData; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.utils.Base58; |
||||
import org.qortal.utils.BitTwiddling; |
||||
|
||||
import com.google.common.hash.HashCode; |
||||
import com.google.common.primitives.Bytes; |
||||
|
||||
/** |
||||
* Cross-chain trade AT |
||||
* |
||||
* <p> |
||||
* <ul> |
||||
* <li>Bob generates Litecoin & Qortal 'trade' keys |
||||
* <ul> |
||||
* <li>private key required to sign P2SH redeem tx</li> |
||||
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li> |
||||
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Bob deploys Qortal AT |
||||
* <ul> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Alice finds Qortal AT and wants to trade |
||||
* <ul> |
||||
* <li>Alice generates Litecoin & Qortal 'trade' keys</li> |
||||
* <li>Alice funds Litecoin P2SH-A</li> |
||||
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing: |
||||
* <ul> |
||||
* <li>hash-of-secret-A</li> |
||||
* <li>her 'trade' Litecoin PKH</li> |
||||
* </ul> |
||||
* </li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Bob receives "offer" MESSAGE |
||||
* <ul> |
||||
* <li>Checks Alice's P2SH-A</li> |
||||
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing: |
||||
* <ul> |
||||
* <li>Alice's trade Qortal address</li> |
||||
* <li>Alice's trade Litecoin PKH</li> |
||||
* <li>hash-of-secret-A</li> |
||||
* </ul> |
||||
* </li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Alice checks Qortal AT to confirm it's locked to her |
||||
* <ul> |
||||
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing: |
||||
* <ul> |
||||
* <li>secret-A</li> |
||||
* <li>Qortal receiving address of her chosing</li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>AT's QORT funds are sent to Qortal receiving address</li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Bob checks AT, extracts secret-A |
||||
* <ul> |
||||
* <li>Bob redeems P2SH-A using his Litecoin trade key and secret-A</li> |
||||
* <li>P2SH-A LTC funds end up at Litecoin address determined by redeem transaction output(s)</li> |
||||
* </ul> |
||||
* </li> |
||||
* </ul> |
||||
*/ |
||||
public class LitecoinACCTv1 { |
||||
|
||||
public static final int SECRET_LENGTH = 32; |
||||
public static final int MIN_LOCKTIME = 1500000000; |
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("0fb15ad9ad1867dfbcafa51155481aa15d984ff9506f2b428eca4e2a2feac2b3").asBytes(); // SHA256 of AT code bytes
|
||||
|
||||
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */ |
||||
private static final int MODE_VALUE_OFFSET = 61; |
||||
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */ |
||||
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE); |
||||
|
||||
public static class OfferMessageData { |
||||
public byte[] partnerLitecoinPKH; |
||||
public byte[] hashOfSecretA; |
||||
public long lockTimeA; |
||||
} |
||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerLitecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/; |
||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/ |
||||
+ 24 /*partner's Litecoin PKH (padded from 20 to 24)*/ |
||||
+ 8 /*AT trade timeout (minutes)*/ |
||||
+ 24 /*hash of secret-A (padded from 20 to 24)*/ |
||||
+ 8 /*lockTimeA*/; |
||||
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/; |
||||
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/; |
||||
|
||||
private LitecoinACCTv1() { |
||||
} |
||||
|
||||
/** |
||||
* Returns Qortal AT creation bytes for cross-chain trading AT. |
||||
* <p> |
||||
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the |
||||
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator. |
||||
* |
||||
* @param creatorTradeAddress AT creator's trade Qortal address |
||||
* @param litecoinPublicKeyHash 20-byte HASH160 of creator's trade Litecoin public key |
||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT |
||||
* @param litecoinAmount how much LTC the AT creator is expecting to trade |
||||
* @param tradeTimeout suggested timeout for entire trade |
||||
*/ |
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] litecoinPublicKeyHash, long qortAmount, long litecoinAmount, int tradeTimeout) { |
||||
if (litecoinPublicKeyHash.length != 20) |
||||
throw new IllegalArgumentException("Litecoin public key hash should be 20 bytes"); |
||||
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0; |
||||
|
||||
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
||||
|
||||
final int addrCreatorTradeAddress1 = addrCounter++; |
||||
final int addrCreatorTradeAddress2 = addrCounter++; |
||||
final int addrCreatorTradeAddress3 = addrCounter++; |
||||
final int addrCreatorTradeAddress4 = addrCounter++; |
||||
|
||||
final int addrLitecoinPublicKeyHash = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrQortAmount = addrCounter++; |
||||
final int addrLitecoinAmount = addrCounter++; |
||||
final int addrTradeTimeout = addrCounter++; |
||||
|
||||
final int addrMessageTxnType = addrCounter++; |
||||
final int addrExpectedTradeMessageLength = addrCounter++; |
||||
final int addrExpectedRedeemMessageLength = addrCounter++; |
||||
|
||||
final int addrCreatorAddressPointer = addrCounter++; |
||||
final int addrQortalPartnerAddressPointer = addrCounter++; |
||||
final int addrMessageSenderPointer = addrCounter++; |
||||
|
||||
final int addrTradeMessagePartnerLitecoinPKHOffset = addrCounter++; |
||||
final int addrPartnerLitecoinPKHPointer = addrCounter++; |
||||
final int addrTradeMessageHashOfSecretAOffset = addrCounter++; |
||||
final int addrHashOfSecretAPointer = addrCounter++; |
||||
|
||||
final int addrRedeemMessageReceivingAddressOffset = addrCounter++; |
||||
|
||||
final int addrMessageDataPointer = addrCounter++; |
||||
final int addrMessageDataLength = addrCounter++; |
||||
|
||||
final int addrPartnerReceivingAddressPointer = addrCounter++; |
||||
|
||||
final int addrEndOfConstants = addrCounter; |
||||
|
||||
// Variables
|
||||
|
||||
final int addrCreatorAddress1 = addrCounter++; |
||||
final int addrCreatorAddress2 = addrCounter++; |
||||
final int addrCreatorAddress3 = addrCounter++; |
||||
final int addrCreatorAddress4 = addrCounter++; |
||||
|
||||
final int addrQortalPartnerAddress1 = addrCounter++; |
||||
final int addrQortalPartnerAddress2 = addrCounter++; |
||||
final int addrQortalPartnerAddress3 = addrCounter++; |
||||
final int addrQortalPartnerAddress4 = addrCounter++; |
||||
|
||||
final int addrLockTimeA = addrCounter++; |
||||
final int addrRefundTimeout = addrCounter++; |
||||
final int addrRefundTimestamp = addrCounter++; |
||||
final int addrLastTxnTimestamp = addrCounter++; |
||||
final int addrBlockTimestamp = addrCounter++; |
||||
final int addrTxnType = addrCounter++; |
||||
final int addrResult = addrCounter++; |
||||
|
||||
final int addrMessageSender1 = addrCounter++; |
||||
final int addrMessageSender2 = addrCounter++; |
||||
final int addrMessageSender3 = addrCounter++; |
||||
final int addrMessageSender4 = addrCounter++; |
||||
|
||||
final int addrMessageLength = addrCounter++; |
||||
|
||||
final int addrMessageData = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrHashOfSecretA = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrPartnerLitecoinPKH = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrPartnerReceivingAddress = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrMode = addrCounter++; |
||||
assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET); |
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); |
||||
|
||||
// AT creator's trade Qortal address, decoded from Base58
|
||||
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect"; |
||||
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress); |
||||
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0)); |
||||
|
||||
// Litecoin public key hash
|
||||
assert dataByteBuffer.position() == addrLitecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrLitecoinPublicKeyHash incorrect"; |
||||
dataByteBuffer.put(Bytes.ensureCapacity(litecoinPublicKeyHash, 32, 0)); |
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect"; |
||||
dataByteBuffer.putLong(qortAmount); |
||||
|
||||
// Expected Litecoin amount
|
||||
assert dataByteBuffer.position() == addrLitecoinAmount * MachineState.VALUE_SIZE : "addrLitecoinAmount incorrect"; |
||||
dataByteBuffer.putLong(litecoinAmount); |
||||
|
||||
// Suggested trade timeout (minutes)
|
||||
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; |
||||
dataByteBuffer.putLong(tradeTimeout); |
||||
|
||||
// We're only interested in MESSAGE transactions
|
||||
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect"; |
||||
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value); |
||||
|
||||
// Expected length of 'trade' MESSAGE data from AT creator
|
||||
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect"; |
||||
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH); |
||||
|
||||
// Expected length of 'redeem' MESSAGE data from trade partner
|
||||
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect"; |
||||
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH); |
||||
|
||||
// Index into data segment of AT creator's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect"; |
||||
dataByteBuffer.putLong(addrCreatorAddress1); |
||||
|
||||
// Index into data segment of partner's Qortal address, used by SET_B_IND
|
||||
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect"; |
||||
dataByteBuffer.putLong(addrQortalPartnerAddress1); |
||||
|
||||
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect"; |
||||
dataByteBuffer.putLong(addrMessageSender1); |
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting partner's Litecoin PKH
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerLitecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerLitecoinPKHOffset incorrect"; |
||||
dataByteBuffer.putLong(32L); |
||||
|
||||
// Index into data segment of partner's Litecoin PKH, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerLitecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerLitecoinPKHPointer incorrect"; |
||||
dataByteBuffer.putLong(addrPartnerLitecoinPKH); |
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect"; |
||||
dataByteBuffer.putLong(64L); |
||||
|
||||
// Index into data segment to hash of secret A, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect"; |
||||
dataByteBuffer.putLong(addrHashOfSecretA); |
||||
|
||||
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
|
||||
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect"; |
||||
dataByteBuffer.putLong(32L); |
||||
|
||||
// Source location and length for hashing any passed secret
|
||||
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect"; |
||||
dataByteBuffer.putLong(addrMessageData); |
||||
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect"; |
||||
dataByteBuffer.putLong(32L); |
||||
|
||||
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect"; |
||||
dataByteBuffer.putLong(addrPartnerReceivingAddress); |
||||
|
||||
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants"; |
||||
|
||||
// Code labels
|
||||
Integer labelRefund = null; |
||||
|
||||
Integer labelTradeTxnLoop = null; |
||||
Integer labelCheckTradeTxn = null; |
||||
Integer labelCheckCancelTxn = null; |
||||
Integer labelNotTradeNorCancelTxn = null; |
||||
Integer labelCheckNonRefundTradeTxn = null; |
||||
Integer labelTradeTxnExtract = null; |
||||
Integer labelRedeemTxnLoop = null; |
||||
Integer labelCheckRedeemTxn = null; |
||||
Integer labelCheckRedeemTxnSender = null; |
||||
Integer labelPayout = null; |
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768); |
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) { |
||||
codeByteBuffer.clear(); |
||||
|
||||
try { |
||||
/* Initialization */ |
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp)); |
||||
|
||||
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B)); |
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer)); |
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile()); |
||||
|
||||
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */ |
||||
|
||||
/* Transaction processing loop */ |
||||
labelTradeTxnLoop = codeByteBuffer.position(); |
||||
|
||||
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp)); |
||||
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult)); |
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn))); |
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile()); |
||||
|
||||
/* Check transaction */ |
||||
labelCheckTradeTxn = codeByteBuffer.position(); |
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp)); |
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType)); |
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop))); |
||||
|
||||
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */ |
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer)); |
||||
// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
// Message sender's address matches AT creator's trade address so go process 'trade' message
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn)); |
||||
|
||||
/* Checking message sender for possible cancel message */ |
||||
labelCheckCancelTxn = codeByteBuffer.position(); |
||||
|
||||
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
// Partner address is AT creator's address, so cancel offer and finish.
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value)); |
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile()); |
||||
|
||||
/* Not trade nor cancel message */ |
||||
labelNotTradeNorCancelTxn = codeByteBuffer.position(); |
||||
|
||||
// Loop to find another transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop)); |
||||
|
||||
/* Possible switch-to-trade-mode message */ |
||||
labelCheckNonRefundTradeTxn = codeByteBuffer.position(); |
||||
|
||||
// Check 'trade' message we received has expected number of message bytes
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength)); |
||||
// If message length matches, branch to info extraction code
|
||||
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract))); |
||||
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop)); |
||||
|
||||
/* Extracting info from 'trade' MESSAGE transaction */ |
||||
labelTradeTxnExtract = codeByteBuffer.position(); |
||||
|
||||
// Extract message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer)); |
||||
|
||||
// Extract trade partner's Litecoin public key hash (PKH) from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerLitecoinPKHOffset)); |
||||
// Store partner's Litecoin PKH (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerLitecoinPKHPointer)); |
||||
// Extract AT trade timeout (minutes) (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout)); |
||||
|
||||
// Grab next 32 bytes
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset)); |
||||
|
||||
// Extract hash-of-secret-A (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer)); |
||||
// Extract lockTime-A (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA)); |
||||
|
||||
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout)); |
||||
|
||||
/* We are in 'trade mode' */ |
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value)); |
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile()); |
||||
|
||||
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */ |
||||
|
||||
// Fetch current block 'timestamp'
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp)); |
||||
// If we're not past refund 'timestamp' then look for next transaction
|
||||
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
// We're past refund 'timestamp' so go refund everything back to AT creator
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund)); |
||||
|
||||
/* Transaction processing loop */ |
||||
labelRedeemTxnLoop = codeByteBuffer.position(); |
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp)); |
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult)); |
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn))); |
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile()); |
||||
|
||||
/* Check transaction */ |
||||
labelCheckRedeemTxn = codeByteBuffer.position(); |
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp)); |
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType)); |
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
|
||||
/* Check message payload length */ |
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength)); |
||||
// If message length matches, branch to sender checking code
|
||||
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender))); |
||||
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop)); |
||||
|
||||
/* Check transaction's sender */ |
||||
labelCheckRedeemTxnSender = codeByteBuffer.position(); |
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer)); |
||||
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
|
||||
/* Check 'secret-A' in transaction's message */ |
||||
|
||||
// Extract secret-A from first 32 bytes of message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer)); |
||||
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer)); |
||||
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
|
||||
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength)); |
||||
// If hashes don't match, addrResult will be zero so go find another transaction
|
||||
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout))); |
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop)); |
||||
|
||||
/* Success! Pay arranged amount to receiving address */ |
||||
labelPayout = codeByteBuffer.position(); |
||||
|
||||
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset)); |
||||
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer)); |
||||
// Pay AT's balance to receiving address
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount)); |
||||
// Set redeemed mode
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value)); |
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile()); |
||||
|
||||
// Fall-through to refunding any remaining balance back to AT creator
|
||||
|
||||
/* Refund balance back to AT creator */ |
||||
labelRefund = codeByteBuffer.position(); |
||||
|
||||
// Set refunded mode
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value)); |
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile()); |
||||
} catch (CompilationException e) { |
||||
throw new IllegalStateException("Unable to compile LTC-QORT ACCT?", e); |
||||
} |
||||
} |
||||
|
||||
codeByteBuffer.flip(); |
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()]; |
||||
codeByteBuffer.get(codeBytes); |
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), LitecoinACCTv1.CODE_BYTES_HASH) |
||||
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes))); |
||||
|
||||
final short ciyamAtVersion = 2; |
||||
final short numCallStackPages = 0; |
||||
final short numUserStackPages = 0; |
||||
final long minActivationAmount = 0L; |
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); |
||||
} |
||||
|
||||
/** |
||||
* Returns CrossChainTradeData with useful info extracted from AT. |
||||
* |
||||
* @param repository |
||||
* @param atAddress |
||||
* @throws DataException |
||||
*/ |
||||
public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException { |
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress()); |
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData); |
||||
} |
||||
|
||||
/** |
||||
* Returns CrossChainTradeData with useful info extracted from AT. |
||||
* |
||||
* @param repository |
||||
* @param atAddress |
||||
* @throws DataException |
||||
*/ |
||||
public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException { |
||||
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress()); |
||||
return populateTradeData(repository, creatorPublicKey, atStateData); |
||||
} |
||||
|
||||
/** |
||||
* Returns CrossChainTradeData with useful info extracted from AT. |
||||
* |
||||
* @param repository |
||||
* @param atAddress |
||||
* @throws DataException |
||||
*/ |
||||
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException { |
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress(); |
||||
|
||||
CrossChainTradeData tradeData = new CrossChainTradeData(); |
||||
|
||||
tradeData.qortalAtAddress = atAddress; |
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey); |
||||
tradeData.creationTimestamp = atStateData.getCreation(); |
||||
|
||||
Account atAccount = new Account(repository, atAddress); |
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT); |
||||
|
||||
byte[] stateData = atStateData.getStateData(); |
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData); |
||||
dataByteBuffer.position(MachineState.HEADER_LENGTH); |
||||
|
||||
/* Constants */ |
||||
|
||||
// Skip creator's trade address
|
||||
dataByteBuffer.get(addressBytes); |
||||
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length); |
||||
|
||||
// Creator's Litecoin/foreign public key hash
|
||||
tradeData.creatorBitcoinPKH = new byte[20]; |
||||
dataByteBuffer.get(tradeData.creatorBitcoinPKH); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorBitcoinPKH.length); // skip to 32 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null; |
||||
|
||||
// Redeem payout
|
||||
tradeData.qortAmount = dataByteBuffer.getLong(); |
||||
|
||||
// Expected LTC amount
|
||||
tradeData.expectedBitcoin = dataByteBuffer.getLong(); |
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong(); |
||||
|
||||
// Skip MESSAGE transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip expected 'trade' message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip expected 'redeem' message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to creator's address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to partner's Qortal trade address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip 'trade' message data offset for partner's Litecoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to partner's Litecoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip 'trade' message data offset for hash-of-secret-A
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to hash-of-secret-A
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip 'redeem' message data offset for partner's Qortal receiving address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip message data length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to partner's receiving address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
/* End of constants / begin variables */ |
||||
|
||||
// Skip AT creator's address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); |
||||
|
||||
// Partner's trade address (if present)
|
||||
dataByteBuffer.get(addressBytes); |
||||
String qortalRecipient = Base58.encode(addressBytes); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length); |
||||
|
||||
// Potential lockTimeA (if in trade mode)
|
||||
int lockTimeA = (int) dataByteBuffer.getLong(); |
||||
|
||||
// AT refund timeout (probably only useful for debugging)
|
||||
int refundTimeout = (int) dataByteBuffer.getLong(); |
||||
|
||||
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
|
||||
long tradeRefundTimestamp = dataByteBuffer.getLong(); |
||||
|
||||
// Skip last transaction timestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip block timestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip temporary result
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip temporary message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); |
||||
|
||||
// Skip message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip temporary message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); |
||||
|
||||
// Potential hash160 of secret A
|
||||
byte[] hashOfSecretA = new byte[20]; |
||||
dataByteBuffer.get(hashOfSecretA); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
||||
|
||||
// Potential partner's Litecoin PKH
|
||||
byte[] partnerLitecoinPKH = new byte[20]; |
||||
dataByteBuffer.get(partnerLitecoinPKH); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerLitecoinPKH.length); // skip to 32 bytes
|
||||
|
||||
// Partner's receiving address (if present)
|
||||
byte[] partnerReceivingAddress = new byte[25]; |
||||
dataByteBuffer.get(partnerReceivingAddress); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
|
||||
|
||||
// Trade AT's 'mode'
|
||||
long modeValue = dataByteBuffer.getLong(); |
||||
AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL)); |
||||
|
||||
/* End of variables */ |
||||
|
||||
if (mode != null && mode != AcctMode.OFFERING) { |
||||
tradeData.mode = mode; |
||||
tradeData.refundTimeout = refundTimeout; |
||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; |
||||
tradeData.qortalPartnerAddress = qortalRecipient; |
||||
tradeData.hashOfSecretA = hashOfSecretA; |
||||
tradeData.partnerBitcoinPKH = partnerLitecoinPKH; |
||||
tradeData.lockTimeA = lockTimeA; |
||||
|
||||
if (mode == AcctMode.REDEEMED) |
||||
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress); |
||||
} else { |
||||
tradeData.mode = AcctMode.OFFERING; |
||||
} |
||||
|
||||
return tradeData; |
||||
} |
||||
|
||||
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */ |
||||
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) { |
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); |
||||
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes); |
||||
} |
||||
|
||||
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */ |
||||
public static OfferMessageData extractOfferMessageData(byte[] messageData) { |
||||
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH) |
||||
return null; |
||||
|
||||
OfferMessageData offerMessageData = new OfferMessageData(); |
||||
offerMessageData.partnerLitecoinPKH = Arrays.copyOfRange(messageData, 0, 20); |
||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40); |
||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40); |
||||
|
||||
return offerMessageData; |
||||
} |
||||
|
||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */ |
||||
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) { |
||||
byte[] data = new byte[TRADE_MESSAGE_LENGTH]; |
||||
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress); |
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); |
||||
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout); |
||||
|
||||
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length); |
||||
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length); |
||||
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length); |
||||
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length); |
||||
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length); |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */ |
||||
public static byte[] buildCancelMessage(String creatorQortalAddress) { |
||||
byte[] data = new byte[CANCEL_MESSAGE_LENGTH]; |
||||
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress); |
||||
|
||||
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length); |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */ |
||||
public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) { |
||||
byte[] data = new byte[REDEEM_MESSAGE_LENGTH]; |
||||
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress); |
||||
|
||||
System.arraycopy(secretA, 0, data, 0, secretA.length); |
||||
System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length); |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */ |
||||
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) { |
||||
// refund should be triggered halfway between offerMessageTimesamp and lockTimeA
|
||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L); |
||||
} |
||||
|
||||
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException { |
||||
String atAddress = crossChainTradeData.qortalAtAddress; |
||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress; |
||||
|
||||
// We don't have partner's public key so we check every message to AT
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null); |
||||
if (messageTransactionsData == null) |
||||
return null; |
||||
|
||||
// Find 'redeem' message
|
||||
for (MessageTransactionData messageTransactionData : messageTransactionsData) { |
||||
// Check message payload type/encryption
|
||||
if (messageTransactionData.isText() || messageTransactionData.isEncrypted()) |
||||
continue; |
||||
|
||||
// Check message payload size
|
||||
byte[] messageData = messageTransactionData.getData(); |
||||
if (messageData.length != REDEEM_MESSAGE_LENGTH) |
||||
// Wrong payload length
|
||||
continue; |
||||
|
||||
// Check sender
|
||||
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress)) |
||||
// Wrong sender;
|
||||
continue; |
||||
|
||||
// Extract secretA
|
||||
byte[] secretA = new byte[32]; |
||||
System.arraycopy(messageData, 0, secretA, 0, secretA.length); |
||||
|
||||
byte[] hashOfSecretA = Crypto.hash160(secretA); |
||||
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA)) |
||||
continue; |
||||
|
||||
return secretA; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,150 @@
|
||||
package org.qortal.test.crosschain.litecoinv1; |
||||
|
||||
import java.math.BigDecimal; |
||||
|
||||
import org.bitcoinj.core.ECKey; |
||||
import org.qortal.account.PrivateKeyAccount; |
||||
import org.qortal.asset.Asset; |
||||
import org.qortal.controller.Controller; |
||||
import org.qortal.crosschain.LitecoinACCTv1; |
||||
import org.qortal.data.transaction.BaseTransactionData; |
||||
import org.qortal.data.transaction.DeployAtTransactionData; |
||||
import org.qortal.group.Group; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.repository.RepositoryFactory; |
||||
import org.qortal.repository.RepositoryManager; |
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; |
||||
import org.qortal.test.crosschain.apps.Common; |
||||
import org.qortal.transaction.DeployAtTransaction; |
||||
import org.qortal.transform.TransformationException; |
||||
import org.qortal.transform.transaction.TransactionTransformer; |
||||
import org.qortal.utils.Amounts; |
||||
import org.qortal.utils.Base58; |
||||
|
||||
import com.google.common.hash.HashCode; |
||||
|
||||
public class DeployAT { |
||||
|
||||
private static void usage(String error) { |
||||
if (error != null) |
||||
System.err.println(error); |
||||
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <AT funding amount> <LTC amount> <trade-timeout>")); |
||||
System.err.println("A trading key-pair will be generated for you!"); |
||||
System.err.println(String.format("example: DeployAT " |
||||
+ "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n" |
||||
+ "\t10 \\\n" |
||||
+ "\t10.1 \\\n" |
||||
+ "\t0.00864200 \\\n" |
||||
+ "\t120")); |
||||
System.exit(1); |
||||
} |
||||
|
||||
public static void main(String[] args) { |
||||
if (args.length != 5) |
||||
usage(null); |
||||
|
||||
Common.init(); |
||||
|
||||
byte[] creatorPrivateKey = null; |
||||
long redeemAmount = 0; |
||||
long fundingAmount = 0; |
||||
long expectedLitecoin = 0; |
||||
int tradeTimeout = 0; |
||||
|
||||
int argIndex = 0; |
||||
try { |
||||
creatorPrivateKey = Base58.decode(args[argIndex++]); |
||||
if (creatorPrivateKey.length != 32) |
||||
usage("Refund private key must be 32 bytes"); |
||||
|
||||
redeemAmount = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue(); |
||||
if (redeemAmount <= 0) |
||||
usage("QORT amount must be positive"); |
||||
|
||||
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue(); |
||||
if (fundingAmount <= redeemAmount) |
||||
usage("AT funding amount must be greater than QORT redeem amount"); |
||||
|
||||
expectedLitecoin = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue(); |
||||
if (expectedLitecoin <= 0) |
||||
usage("Expected LTC amount must be positive"); |
||||
|
||||
tradeTimeout = Integer.parseInt(args[argIndex++]); |
||||
if (tradeTimeout < 60 || tradeTimeout > 50000) |
||||
usage("Trade timeout (minutes) must be between 60 and 50000"); |
||||
} catch (IllegalArgumentException e) { |
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); |
||||
} |
||||
|
||||
try { |
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); |
||||
RepositoryManager.setRepositoryFactory(repositoryFactory); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) { |
||||
PrivateKeyAccount creatorAccount = new PrivateKeyAccount(repository, creatorPrivateKey); |
||||
System.out.println(String.format("Creator Qortal address: %s", creatorAccount.getAddress())); |
||||
System.out.println(String.format("QORT redeem amount: %s", Amounts.prettyAmount(redeemAmount))); |
||||
System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount))); |
||||
|
||||
// Generate trading key-pair
|
||||
byte[] tradePrivateKey = new ECKey().getPrivKeyBytes(); |
||||
PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey); |
||||
byte[] litecoinPublicKeyHash = ECKey.fromPrivate(tradePrivateKey).getPubKeyHash(); |
||||
|
||||
System.out.println(String.format("Trade private key: %s", HashCode.fromBytes(tradePrivateKey))); |
||||
|
||||
// Deploy AT
|
||||
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, expectedLitecoin, tradeTimeout); |
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); |
||||
|
||||
long txTimestamp = System.currentTimeMillis(); |
||||
byte[] lastReference = creatorAccount.getLastReference(); |
||||
|
||||
if (lastReference == null) { |
||||
System.err.println(String.format("Qortal account %s has no last reference", creatorAccount.getAddress())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
Long fee = null; |
||||
String name = "QORT-LTC cross-chain trade"; |
||||
String description = String.format("Qortal-Litecoin cross-chain trade"); |
||||
String atType = "ACCT"; |
||||
String tags = "QORT-LTC ACCT"; |
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null); |
||||
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); |
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); |
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee(); |
||||
deployAtTransactionData.setFee(fee); |
||||
|
||||
deployAtTransaction.sign(creatorAccount); |
||||
|
||||
byte[] signedBytes = null; |
||||
try { |
||||
signedBytes = TransactionTransformer.toBytes(deployAtTransactionData); |
||||
} catch (TransformationException e) { |
||||
System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
DeployAtTransaction.ensureATAddress(deployAtTransactionData); |
||||
String atAddress = deployAtTransactionData.getAtAddress(); |
||||
|
||||
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); |
||||
|
||||
System.out.println(String.format("AT address: %s", atAddress)); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,770 @@
|
||||
package org.qortal.test.crosschain.litecoinv1; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
import java.time.Instant; |
||||
import java.time.LocalDateTime; |
||||
import java.time.ZoneOffset; |
||||
import java.time.format.DateTimeFormatter; |
||||
import java.time.format.FormatStyle; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Random; |
||||
import java.util.function.Function; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.qortal.account.Account; |
||||
import org.qortal.account.PrivateKeyAccount; |
||||
import org.qortal.asset.Asset; |
||||
import org.qortal.block.Block; |
||||
import org.qortal.crosschain.LitecoinACCTv1; |
||||
import org.qortal.crosschain.AcctMode; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.data.at.ATData; |
||||
import org.qortal.data.at.ATStateData; |
||||
import org.qortal.data.crosschain.CrossChainTradeData; |
||||
import org.qortal.data.transaction.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.utils.Amounts; |
||||
|
||||
import com.google.common.hash.HashCode; |
||||
import com.google.common.primitives.Bytes; |
||||
|
||||
public class LitecoinACCTv1Tests extends Common { |
||||
|
||||
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes(); |
||||
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||
public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); |
||||
public static final int tradeTimeout = 20; // blocks
|
||||
public static final long redeemAmount = 80_40200000L; |
||||
public static final long fundingAmount = 123_45600000L; |
||||
public static final long litecoinAmount = 864200L; // 0.00864200 LTC
|
||||
|
||||
private static final Random RANDOM = new Random(); |
||||
|
||||
@Before |
||||
public void beforeTest() throws DataException { |
||||
Common.useDefaultSettings(); |
||||
} |
||||
|
||||
@Test |
||||
public void testCompile() { |
||||
PrivateKeyAccount tradeAccount = createTradeAccount(null); |
||||
|
||||
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout); |
||||
assertNotNull(creationBytes); |
||||
|
||||
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void testDeploy() 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()); |
||||
|
||||
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee(); |
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
expectedBalance = fundingAmount; |
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
expectedBalance = partnersInitialBalance; |
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository); |
||||
|
||||
expectedBalance = deployersInitialBalance; |
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
expectedBalance = 0; |
||||
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
expectedBalance = partnersInitialBalance; |
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testOfferCancel() 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 deployAtFee = deployAtTransaction.getTransactionData().getFee(); |
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; |
||||
|
||||
// Send creator's address to AT, instead of typical partner's address
|
||||
byte[] messageData = LitecoinACCTv1.buildCancelMessage(deployer.getAddress()); |
||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); |
||||
long messageFee = messageTransaction.getTransactionData().getFee(); |
||||
|
||||
// AT should process 'cancel' message in next block
|
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
describeAt(repository, atAddress); |
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress); |
||||
assertTrue(atData.getIsFinished()); |
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode); |
||||
|
||||
// Check balances
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance; |
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee; |
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
|
||||
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); |
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanLastBlock(repository); |
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance - messageFee; |
||||
actualBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testOfferCancelInvalidLength() 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 deployAtFee = deployAtTransaction.getTransactionData().getFee(); |
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; |
||||
|
||||
// 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); |
||||
|
||||
describeAt(repository, atAddress); |
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress); |
||||
assertTrue(atData.getIsFinished()); |
||||
|
||||
// AT should be in CANCELLED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.CANCELLED, tradeData.mode); |
||||
} |
||||
} |
||||
|
||||
@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 refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
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 = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
|
||||
// AT should be in TRADE mode
|
||||
assertEquals(AcctMode.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 Litecoin PKH was extracted correctly
|
||||
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerBitcoinPKH)); |
||||
|
||||
// Test orphaning
|
||||
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); |
||||
|
||||
// Check balances
|
||||
long expectedBalance = deployersPostDeploymentBalance; |
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); |
||||
} |
||||
} |
||||
|
||||
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testIncorrectTradeSender() throws DataException { |
||||
try (final Repository repository = RepositoryManager.getRepository()) { |
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); |
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository); |
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); |
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); |
||||
|
||||
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 refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress); |
||||
|
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
long expectedBalance = partnersInitialBalance; |
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
describeAt(repository, atAddress); |
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress); |
||||
CrossChainTradeData tradeData = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
|
||||
// AT should still be in OFFER mode
|
||||
assertEquals(AcctMode.OFFERING, tradeData.mode); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testAutomaticTradeRefund() 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 refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
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 deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; |
||||
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); |
||||
|
||||
describeAt(repository, atAddress); |
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress); |
||||
assertTrue(atData.getIsFinished()); |
||||
|
||||
// AT should be in REFUNDED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.REFUNDED, tradeData.mode); |
||||
|
||||
// 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); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testCorrectSecretCorrectSender() 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 refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); |
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
// Send correct secret to AT, from correct account
|
||||
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress()); |
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress); |
||||
|
||||
// AT should send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); |
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
describeAt(repository, atAddress); |
||||
|
||||
// Check AT is finished
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress); |
||||
assertTrue(atData.getIsFinished()); |
||||
|
||||
// AT should be in REDEEMED mode
|
||||
CrossChainTradeData tradeData = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.REDEEMED, tradeData.mode); |
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; |
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
// Orphan redeem
|
||||
BlockUtils.orphanLastBlock(repository); |
||||
|
||||
// Check balances
|
||||
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); |
||||
actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
// Check AT state
|
||||
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress); |
||||
|
||||
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData())); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testCorrectSecretIncorrectSender() throws DataException { |
||||
try (final Repository repository = RepositoryManager.getRepository()) { |
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); |
||||
PrivateKeyAccount tradeAccount = createTradeAccount(repository); |
||||
|
||||
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); |
||||
|
||||
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); |
||||
|
||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); |
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee(); |
||||
|
||||
Account at = deployAtTransaction.getATAccount(); |
||||
String atAddress = at.getAddress(); |
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); |
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); |
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); |
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
// Send correct secret to AT, but from wrong account
|
||||
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress()); |
||||
messageTransaction = sendMessage(repository, bystander, messageData, atAddress); |
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); |
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
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 = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.TRADING, tradeData.mode); |
||||
|
||||
// Check balances
|
||||
long expectedBalance = partnersInitialBalance; |
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testIncorrectSecretCorrectSender() throws DataException { |
||||
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()); |
||||
long deployAtFee = deployAtTransaction.getTransactionData().getFee(); |
||||
|
||||
Account at = deployAtTransaction.getATAccount(); |
||||
String atAddress = at.getAddress(); |
||||
|
||||
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); |
||||
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); |
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); |
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
// Send incorrect secret to AT, from correct account
|
||||
byte[] wrongSecret = new byte[32]; |
||||
RANDOM.nextBytes(wrongSecret); |
||||
messageData = LitecoinACCTv1.buildRedeemMessage(wrongSecret, partner.getAddress()); |
||||
messageTransaction = sendMessage(repository, partner, messageData, atAddress); |
||||
|
||||
// AT should NOT send funds in the next block
|
||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); |
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
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 = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.TRADING, tradeData.mode); |
||||
|
||||
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); |
||||
long actualBalance = partner.getConfirmedBalance(Asset.QORT); |
||||
|
||||
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); |
||||
|
||||
// Check eventual refund
|
||||
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testCorrectSecretCorrectSenderInvalidMessageLength() 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 refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); |
||||
|
||||
// Send trade info to AT
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); |
||||
|
||||
// Give AT time to process message
|
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
|
||||
messageData = Bytes.concat(secretA); |
||||
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 = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
assertEquals(AcctMode.TRADING, tradeData.mode); |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
@Test |
||||
public void testDescribeDeployed() 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()); |
||||
|
||||
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs(); |
||||
|
||||
for (ATData atData : executableAts) { |
||||
String atAddress = atData.getATAddress(); |
||||
byte[] codeBytes = atData.getCodeBytes(); |
||||
byte[] codeHash = Crypto.digest(codeBytes); |
||||
|
||||
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s", |
||||
atAddress, |
||||
codeBytes.length, |
||||
(codeBytes.length != 1 ? "s": ""), |
||||
HashCode.fromBytes(codeHash))); |
||||
|
||||
// Not one of ours?
|
||||
if (!Arrays.equals(codeHash, LitecoinACCTv1.CODE_BYTES_HASH)) |
||||
continue; |
||||
|
||||
describeAt(repository, atAddress); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private int calcTestLockTimeA(long messageTimestamp) { |
||||
return (int) (messageTimestamp / 1000L + tradeTimeout * 60); |
||||
} |
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException { |
||||
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout); |
||||
|
||||
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 = "QORT-LTC cross-chain trade"; |
||||
String description = String.format("Qortal-Litecoin cross-chain trade"); |
||||
String atType = "ACCT"; |
||||
String tags = "QORT-LTC ACCT"; |
||||
|
||||
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; |
||||
} |
||||
|
||||
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException { |
||||
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; |
||||
int refundTimeout = tradeTimeout / 2 + 1; // close enough
|
||||
|
||||
// AT should automatically refund deployer after 'refundTimeout' blocks
|
||||
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount) |
||||
BlockUtils.mintBlock(repository); |
||||
|
||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||
long expectedMinimumBalance = deployersPostDeploymentBalance; |
||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee; |
||||
|
||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT); |
||||
|
||||
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); |
||||
} |
||||
|
||||
private void describeAt(Repository repository, String atAddress) throws DataException { |
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress); |
||||
CrossChainTradeData tradeData = LitecoinACCTv1.populateTradeData(repository, atData); |
||||
|
||||
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); |
||||
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); |
||||
|
||||
System.out.print(String.format("%s:\n" |
||||
+ "\tmode: %s\n" |
||||
+ "\tcreator: %s,\n" |
||||
+ "\tcreation timestamp: %s,\n" |
||||
+ "\tcurrent balance: %s QORT,\n" |
||||
+ "\tis finished: %b,\n" |
||||
+ "\tredeem payout: %s QORT,\n" |
||||
+ "\texpected Litecoin: %s LTC,\n" |
||||
+ "\tcurrent block height: %d,\n", |
||||
tradeData.qortalAtAddress, |
||||
tradeData.mode, |
||||
tradeData.qortalCreator, |
||||
epochMilliFormatter.apply(tradeData.creationTimestamp), |
||||
Amounts.prettyAmount(tradeData.qortBalance), |
||||
atData.getIsFinished(), |
||||
Amounts.prettyAmount(tradeData.qortAmount), |
||||
Amounts.prettyAmount(tradeData.expectedBitcoin), |
||||
currentBlockHeight)); |
||||
|
||||
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) { |
||||
System.out.println(String.format("\trefund timeout: %d minutes,\n" |
||||
+ "\trefund height: block %d,\n" |
||||
+ "\tHASH160 of secret-A: %s,\n" |
||||
+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n" |
||||
+ "\ttrade partner: %s\n" |
||||
+ "\tpartner's receiving address: %s", |
||||
tradeData.refundTimeout, |
||||
tradeData.tradeRefundHeight, |
||||
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40), |
||||
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L), |
||||
tradeData.qortalPartnerAddress, |
||||
tradeData.qortalPartnerReceivingAddress)); |
||||
} |
||||
} |
||||
|
||||
private PrivateKeyAccount createTradeAccount(Repository repository) { |
||||
// We actually use a known test account with funds to avoid PoW compute
|
||||
return Common.getTestAccount(repository, "alice"); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,90 @@
|
||||
package org.qortal.test.crosschain.litecoinv1; |
||||
|
||||
import org.qortal.account.PrivateKeyAccount; |
||||
import org.qortal.controller.Controller; |
||||
import org.qortal.crosschain.LitecoinACCTv1; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.group.Group; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.repository.RepositoryFactory; |
||||
import org.qortal.repository.RepositoryManager; |
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; |
||||
import org.qortal.test.crosschain.apps.Common; |
||||
import org.qortal.transaction.MessageTransaction; |
||||
import org.qortal.transform.TransformationException; |
||||
import org.qortal.transform.transaction.TransactionTransformer; |
||||
import org.qortal.utils.Base58; |
||||
|
||||
public class SendCancelMessage { |
||||
|
||||
private static void usage(String error) { |
||||
if (error != null) |
||||
System.err.println(error); |
||||
|
||||
System.err.println(String.format("usage: SendCancelMessage <your Qortal PRIVATE key> <AT address>")); |
||||
System.err.println(String.format("example: SendCancelMessage " |
||||
+ "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n" |
||||
+ "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); |
||||
System.exit(1); |
||||
} |
||||
|
||||
public static void main(String[] args) { |
||||
if (args.length != 2) |
||||
usage(null); |
||||
|
||||
Common.init(); |
||||
|
||||
byte[] qortalPrivateKey = null; |
||||
String atAddress = null; |
||||
|
||||
int argIndex = 0; |
||||
try { |
||||
qortalPrivateKey = Base58.decode(args[argIndex++]); |
||||
if (qortalPrivateKey.length != 32) |
||||
usage("Refund private key must be 32 bytes"); |
||||
|
||||
atAddress = args[argIndex++]; |
||||
if (!Crypto.isValidAtAddress(atAddress)) |
||||
usage("Invalid AT address"); |
||||
} catch (IllegalArgumentException e) { |
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); |
||||
} |
||||
|
||||
try { |
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); |
||||
RepositoryManager.setRepositoryFactory(repositoryFactory); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) { |
||||
PrivateKeyAccount qortalAccount = new PrivateKeyAccount(repository, qortalPrivateKey); |
||||
|
||||
String creatorQortalAddress = qortalAccount.getAddress(); |
||||
System.out.println(String.format("Qortal address: %s", creatorQortalAddress)); |
||||
|
||||
byte[] messageData = LitecoinACCTv1.buildCancelMessage(creatorQortalAddress); |
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, qortalAccount, Group.NO_GROUP, atAddress, messageData, false, false); |
||||
|
||||
System.out.println("Computing nonce..."); |
||||
messageTransaction.computeNonce(); |
||||
messageTransaction.sign(qortalAccount); |
||||
|
||||
byte[] signedBytes = null; |
||||
try { |
||||
signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData()); |
||||
} catch (TransformationException e) { |
||||
System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,101 @@
|
||||
package org.qortal.test.crosschain.litecoinv1; |
||||
|
||||
import org.qortal.account.PrivateKeyAccount; |
||||
import org.qortal.controller.Controller; |
||||
import org.qortal.crosschain.LitecoinACCTv1; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.group.Group; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.repository.RepositoryFactory; |
||||
import org.qortal.repository.RepositoryManager; |
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; |
||||
import org.qortal.test.crosschain.apps.Common; |
||||
import org.qortal.transaction.MessageTransaction; |
||||
import org.qortal.transform.TransformationException; |
||||
import org.qortal.transform.transaction.TransactionTransformer; |
||||
import org.qortal.utils.Base58; |
||||
|
||||
import com.google.common.hash.HashCode; |
||||
|
||||
public class SendRedeemMessage { |
||||
|
||||
private static void usage(String error) { |
||||
if (error != null) |
||||
System.err.println(error); |
||||
|
||||
System.err.println(String.format("usage: SendRedeemMessage <partner trade PRIVATE key> <AT address> <secret> <Qortal receive address>")); |
||||
System.err.println(String.format("example: SendRedeemMessage " |
||||
+ "dbfe739f5a3ecf7b0a22cea71f73d86ec71355b740e5972bcdf9e8bb4721ab9d \\\n" |
||||
+ "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n" |
||||
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n" |
||||
+ "\tQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")); |
||||
System.exit(1); |
||||
} |
||||
|
||||
public static void main(String[] args) { |
||||
if (args.length != 4) |
||||
usage(null); |
||||
|
||||
Common.init(); |
||||
|
||||
byte[] tradePrivateKey = null; |
||||
String atAddress = null; |
||||
byte[] secret = null; |
||||
String receiveAddress = null; |
||||
|
||||
int argIndex = 0; |
||||
try { |
||||
tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); |
||||
if (tradePrivateKey.length != 32) |
||||
usage("Refund private key must be 32 bytes"); |
||||
|
||||
atAddress = args[argIndex++]; |
||||
if (!Crypto.isValidAtAddress(atAddress)) |
||||
usage("Invalid AT address"); |
||||
|
||||
secret = HashCode.fromString(args[argIndex++]).asBytes(); |
||||
if (secret.length != 32) |
||||
usage("Secret must be 32 bytes"); |
||||
|
||||
receiveAddress = args[argIndex++]; |
||||
if (!Crypto.isValidAddress(receiveAddress)) |
||||
usage("Invalid Qortal receive address"); |
||||
} catch (IllegalArgumentException e) { |
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); |
||||
} |
||||
|
||||
try { |
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); |
||||
RepositoryManager.setRepositoryFactory(repositoryFactory); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) { |
||||
PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey); |
||||
|
||||
byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secret, receiveAddress); |
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false); |
||||
|
||||
System.out.println("Computing nonce..."); |
||||
messageTransaction.computeNonce(); |
||||
messageTransaction.sign(tradeAccount); |
||||
|
||||
byte[] signedBytes = null; |
||||
try { |
||||
signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData()); |
||||
} catch (TransformationException e) { |
||||
System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,118 @@
|
||||
package org.qortal.test.crosschain.litecoinv1; |
||||
|
||||
import org.qortal.account.PrivateKeyAccount; |
||||
import org.qortal.controller.Controller; |
||||
import org.qortal.crosschain.LitecoinACCTv1; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.group.Group; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.repository.RepositoryFactory; |
||||
import org.qortal.repository.RepositoryManager; |
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; |
||||
import org.qortal.test.crosschain.apps.Common; |
||||
import org.qortal.transaction.MessageTransaction; |
||||
import org.qortal.transform.TransformationException; |
||||
import org.qortal.transform.transaction.TransactionTransformer; |
||||
import org.qortal.utils.Base58; |
||||
import org.qortal.utils.NTP; |
||||
|
||||
import com.google.common.hash.HashCode; |
||||
|
||||
public class SendTradeMessage { |
||||
|
||||
private static void usage(String error) { |
||||
if (error != null) |
||||
System.err.println(error); |
||||
|
||||
System.err.println(String.format("usage: SendTradeMessage <trade PRIVATE key> <AT address> <partner trade Qortal address> <partner tradeLitecoin PKH/P2PKH> <hash-of-secret> <locktime>")); |
||||
System.err.println(String.format("example: SendTradeMessage " |
||||
+ "ed77aa2c62d785a9428725fc7f95b907be8a1cc43213239876a62cf70fdb6ecb \\\n" |
||||
+ "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n" |
||||
+ "\tQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq \\\n" |
||||
+ "\tffffffffffffffffffffffffffffffffffffffff \\\n" |
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" |
||||
+ "\t1600184800")); |
||||
System.exit(1); |
||||
} |
||||
|
||||
public static void main(String[] args) { |
||||
if (args.length != 6) |
||||
usage(null); |
||||
|
||||
Common.init(); |
||||
|
||||
byte[] tradePrivateKey = null; |
||||
String atAddress = null; |
||||
String partnerTradeAddress = null; |
||||
byte[] partnerTradePublicKeyHash = null; |
||||
byte[] secretHash = null; |
||||
int lockTime = 0; |
||||
|
||||
int argIndex = 0; |
||||
try { |
||||
tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); |
||||
if (tradePrivateKey.length != 32) |
||||
usage("Refund private key must be 32 bytes"); |
||||
|
||||
atAddress = args[argIndex++]; |
||||
if (!Crypto.isValidAtAddress(atAddress)) |
||||
usage("Invalid AT address"); |
||||
|
||||
partnerTradeAddress = args[argIndex++]; |
||||
if (!Crypto.isValidAddress(partnerTradeAddress)) |
||||
usage("Invalid partner trade Qortal address"); |
||||
|
||||
partnerTradePublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes(); |
||||
if (partnerTradePublicKeyHash.length != 20) |
||||
usage("Partner trade PKH must be 20 bytes"); |
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes(); |
||||
if (secretHash.length != 20) |
||||
usage("HASH160 of secret must be 20 bytes"); |
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]); |
||||
} catch (IllegalArgumentException e) { |
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); |
||||
} |
||||
|
||||
try { |
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); |
||||
RepositoryManager.setRepositoryFactory(repositoryFactory); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) { |
||||
PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey); |
||||
|
||||
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(NTP.getTime(), lockTime); |
||||
if (refundTimeout < 1) { |
||||
System.err.println("Refund timeout too small. Is locktime in the past?"); |
||||
System.exit(2); |
||||
} |
||||
|
||||
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partnerTradeAddress, partnerTradePublicKeyHash, secretHash, lockTime, refundTimeout); |
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false); |
||||
|
||||
System.out.println("Computing nonce..."); |
||||
messageTransaction.computeNonce(); |
||||
messageTransaction.sign(tradeAccount); |
||||
|
||||
byte[] signedBytes = null; |
||||
try { |
||||
signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData()); |
||||
} catch (TransformationException e) { |
||||
System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
|
||||
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); |
||||
} catch (DataException e) { |
||||
System.err.println(String.format("Repository issue: %s", e.getMessage())); |
||||
System.exit(2); |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue