forked from Qortal/qortal
CalDescent
3 years ago
4 changed files with 2 additions and 1742 deletions
@ -1,883 +0,0 @@
|
||||
package org.qortal.controller.tradebot; |
||||
|
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
import org.bitcoinj.core.*; |
||||
import org.bitcoinj.script.Script.ScriptType; |
||||
import org.qortal.account.PrivateKeyAccount; |
||||
import org.qortal.account.PublicKeyAccount; |
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest; |
||||
import org.qortal.asset.Asset; |
||||
import org.qortal.crosschain.*; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.data.at.ATData; |
||||
import org.qortal.data.crosschain.CrossChainTradeData; |
||||
import org.qortal.data.crosschain.TradeBotData; |
||||
import org.qortal.data.transaction.BaseTransactionData; |
||||
import org.qortal.data.transaction.DeployAtTransactionData; |
||||
import org.qortal.data.transaction.MessageTransactionData; |
||||
import org.qortal.group.Group; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.transaction.DeployAtTransaction; |
||||
import org.qortal.transaction.MessageTransaction; |
||||
import org.qortal.transaction.Transaction.ValidationResult; |
||||
import org.qortal.transform.TransformationException; |
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer; |
||||
import org.qortal.utils.Base58; |
||||
import org.qortal.utils.NTP; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import static java.util.Arrays.stream; |
||||
import static java.util.stream.Collectors.toMap; |
||||
|
||||
/** |
||||
* Performing cross-chain trading steps on behalf of user. |
||||
* <p> |
||||
* We deal with three different independent state-spaces here: |
||||
* <ul> |
||||
* <li>Qortal blockchain</li> |
||||
* <li>Foreign blockchain</li> |
||||
* <li>Trade-bot entries</li> |
||||
* </ul> |
||||
*/ |
||||
public class DogecoinACCTv2TradeBot implements AcctTradeBot { |
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2TradeBot.class); |
||||
|
||||
public enum State implements TradeBot.StateNameAndValueSupplier { |
||||
BOB_WAITING_FOR_AT_CONFIRM(10, false, false), |
||||
BOB_WAITING_FOR_MESSAGE(15, true, true), |
||||
BOB_WAITING_FOR_AT_REDEEM(25, true, true), |
||||
BOB_DONE(30, false, false), |
||||
BOB_REFUNDED(35, false, false), |
||||
|
||||
ALICE_WAITING_FOR_AT_LOCK(85, true, true), |
||||
ALICE_DONE(95, false, false), |
||||
ALICE_REFUNDING_A(105, true, true), |
||||
ALICE_REFUNDED(110, false, false); |
||||
|
||||
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state)); |
||||
|
||||
public final int value; |
||||
public final boolean requiresAtData; |
||||
public final boolean requiresTradeData; |
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) { |
||||
this.value = value; |
||||
this.requiresAtData = requiresAtData; |
||||
this.requiresTradeData = requiresTradeData; |
||||
} |
||||
|
||||
public static State valueOf(int value) { |
||||
return map.get(value); |
||||
} |
||||
|
||||
@Override |
||||
public String getState() { |
||||
return this.name(); |
||||
} |
||||
|
||||
@Override |
||||
public int getStateValue() { |
||||
return this.value; |
||||
} |
||||
} |
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */ |
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
private static DogecoinACCTv2TradeBot instance; |
||||
|
||||
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream() |
||||
.map(State::name) |
||||
.collect(Collectors.toUnmodifiableList()); |
||||
|
||||
private DogecoinACCTv2TradeBot() { |
||||
} |
||||
|
||||
public static synchronized DogecoinACCTv2TradeBot getInstance() { |
||||
if (instance == null) |
||||
instance = new DogecoinACCTv2TradeBot(); |
||||
|
||||
return instance; |
||||
} |
||||
|
||||
@Override |
||||
public List<String> getEndStates() { |
||||
return this.endStates; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DOGE. |
||||
* <p> |
||||
* Generates: |
||||
* <ul> |
||||
* <li>new 'trade' private key</li> |
||||
* </ul> |
||||
* Derives: |
||||
* <ul> |
||||
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li> |
||||
* <li>'foreign' (as in Dogecoin) public key, public key hash</li> |
||||
* </ul> |
||||
* A Qortal AT is then constructed including the following as constants in the 'data segment': |
||||
* <ul> |
||||
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li> |
||||
* <li>'foreign'/Dogecoin public key hash - used by Alice's P2SH scripts to allow redeem</li> |
||||
* <li>QORT amount on offer by Bob</li> |
||||
* <li>DOGE amount expected in return by Bob (from Alice)</li> |
||||
* <li>trading timeout, in case things go wrong and everyone needs to refund</li> |
||||
* </ul> |
||||
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network. |
||||
* <p> |
||||
* Trade-bot will wait for Bob's AT to be deployed before taking next step. |
||||
* <p> |
||||
* @param repository |
||||
* @param tradeBotCreateRequest |
||||
* @return raw, unsigned DEPLOY_AT transaction |
||||
* @throws DataException |
||||
*/ |
||||
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException { |
||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey(); |
||||
|
||||
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey); |
||||
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey); |
||||
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey); |
||||
|
||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey); |
||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); |
||||
|
||||
// Convert Dogecoin receiving address into public key hash (we only support P2PKH at this time)
|
||||
Address dogecoinReceivingAddress; |
||||
try { |
||||
dogecoinReceivingAddress = Address.fromString(Dogecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); |
||||
} catch (AddressFormatException e) { |
||||
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress); |
||||
} |
||||
if (dogecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH) |
||||
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress); |
||||
|
||||
byte[] dogecoinReceivingAccountInfo = dogecoinReceivingAddress.getHash(); |
||||
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey); |
||||
|
||||
// Deploy AT
|
||||
long timestamp = NTP.getTime(); |
||||
byte[] reference = creator.getLastReference(); |
||||
long fee = 0L; |
||||
byte[] signature = null; |
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature); |
||||
|
||||
String name = "QORT/DOGE ACCT"; |
||||
String description = "QORT/DOGE cross-chain trade"; |
||||
String aTType = "ACCT"; |
||||
String tags = "ACCT QORT DOGE"; |
||||
byte[] creationBytes = DogecoinACCTv2.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount, |
||||
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout); |
||||
long amount = tradeBotCreateRequest.fundingQortAmount; |
||||
|
||||
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT); |
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); |
||||
fee = deployAtTransaction.calcRecommendedFee(); |
||||
deployAtTransactionData.setFee(fee); |
||||
|
||||
DeployAtTransaction.ensureATAddress(deployAtTransactionData); |
||||
String atAddress = deployAtTransactionData.getAtAddress(); |
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv2.NAME, |
||||
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value, |
||||
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount, |
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, |
||||
null, null, |
||||
SupportedBlockchain.DOGECOIN.name(), |
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash, |
||||
tradeBotCreateRequest.foreignAmount, null, null, null, dogecoinReceivingAccountInfo); |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress)); |
||||
|
||||
// Attempt to backup the trade bot data
|
||||
TradeBot.backupTradeBotData(repository); |
||||
|
||||
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||
try { |
||||
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData); |
||||
} catch (TransformationException e) { |
||||
throw new DataException("Failed to transform DEPLOY_AT transaction?", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DOGE to an existing offer. |
||||
* <p> |
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt> |
||||
* and access to a Dogecoin wallet via <tt>xprv58</tt>. |
||||
* <p> |
||||
* The <tt>crossChainTradeData</tt> contains the current trade offer state |
||||
* as extracted from the AT's data segment. |
||||
* <p> |
||||
* Access to a funded wallet is via a Dogecoin BIP32 hierarchical deterministic key, |
||||
* passed via <tt>xprv58</tt>. |
||||
* <b>This key will be stored in your node's database</b> |
||||
* to allow trade-bot to create/fund the necessary P2SH transactions! |
||||
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot |
||||
* only a subset of wallet access (see BIP32 for more details). |
||||
* <p> |
||||
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i> |
||||
* Electrum wallet by going to the console tab and entering:<br> |
||||
* <tt>wallet.keystore.xprv</tt><br> |
||||
* which should result in a base58 string starting with either 'xprv' (for Dogecoin main-net) |
||||
* or 'tprv' for (Dogecoin test-net). |
||||
* <p> |
||||
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet. |
||||
* <p> |
||||
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b> |
||||
* with the Dogecoin amount expected by 'Bob'. |
||||
* <p> |
||||
* If the Dogecoin transaction is successfully broadcast to the network then |
||||
* we also send a MESSAGE to Bob's trade-bot to let them know. |
||||
* <p> |
||||
* The trade-bot entry is saved to the repository and the cross-chain trading process commences. |
||||
* <p> |
||||
* @param repository |
||||
* @param crossChainTradeData chosen trade OFFER that Alice wants to match |
||||
* @param xprv58 funded wallet xprv in base58 |
||||
* @return true if P2SH-A funding transaction successfully broadcast to Dogecoin network, false otherwise |
||||
* @throws DataException |
||||
*/ |
||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException { |
||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey(); |
||||
byte[] secretA = TradeBot.generateSecret(); |
||||
byte[] hashOfSecretA = Crypto.hash160(secretA); |
||||
|
||||
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey); |
||||
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey); |
||||
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey); |
||||
|
||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey); |
||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); |
||||
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||
|
||||
// We need to generate lockTime-A: add tradeTimeout to now
|
||||
long now = NTP.getTime(); |
||||
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L); |
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv2.NAME, |
||||
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value, |
||||
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount, |
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, |
||||
secretA, hashOfSecretA, |
||||
SupportedBlockchain.DOGECOIN.name(), |
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash, |
||||
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash); |
||||
|
||||
// Attempt to backup the trade bot data
|
||||
TradeBot.backupTradeBotData(repository); |
||||
|
||||
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||
long p2shFee; |
||||
try { |
||||
p2shFee = Dogecoin.getInstance().getP2shFee(now); |
||||
} catch (ForeignBlockchainException e) { |
||||
LOGGER.debug("Couldn't estimate Dogecoin fees?"); |
||||
return ResponseResult.NETWORK_ISSUE; |
||||
} |
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/; |
||||
|
||||
// P2SH-A to be funded
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA); |
||||
String p2shAddress = Dogecoin.getInstance().deriveP2shAddress(redeemScriptBytes); |
||||
|
||||
// Build transaction for funding P2SH-A
|
||||
Transaction p2shFundingTransaction = Dogecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA); |
||||
if (p2shFundingTransaction == null) { |
||||
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?"); |
||||
return ResponseResult.BALANCE_ISSUE; |
||||
} |
||||
|
||||
try { |
||||
Dogecoin.getInstance().broadcastTransaction(p2shFundingTransaction); |
||||
} catch (ForeignBlockchainException e) { |
||||
// We couldn't fund P2SH-A at this time
|
||||
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?"); |
||||
return ResponseResult.NETWORK_ISSUE; |
||||
} |
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = DogecoinACCTv2.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); |
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; |
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); |
||||
if (!isMessageAlreadySent) { |
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); |
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); |
||||
|
||||
messageTransaction.computeNonce(); |
||||
messageTransaction.sign(sender); |
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges(); |
||||
ValidationResult result = messageTransaction.importAsUnconfirmed(); |
||||
|
||||
if (result != ValidationResult.OK) { |
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name())); |
||||
return ResponseResult.NETWORK_ISSUE; |
||||
} |
||||
} |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); |
||||
|
||||
return ResponseResult.OK; |
||||
} |
||||
|
||||
@Override |
||||
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException { |
||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue()); |
||||
if (tradeBotState == null) |
||||
return true; |
||||
|
||||
// If the AT doesn't exist then we might as well let the user tidy up
|
||||
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) |
||||
return true; |
||||
|
||||
switch (tradeBotState) { |
||||
case BOB_WAITING_FOR_AT_CONFIRM: |
||||
case ALICE_DONE: |
||||
case BOB_DONE: |
||||
case ALICE_REFUNDED: |
||||
case BOB_REFUNDED: |
||||
return true; |
||||
|
||||
default: |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException { |
||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue()); |
||||
if (tradeBotState == null) { |
||||
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress())); |
||||
return; |
||||
} |
||||
|
||||
ATData atData = null; |
||||
CrossChainTradeData tradeData = null; |
||||
|
||||
if (tradeBotState.requiresAtData) { |
||||
// Attempt to fetch AT data
|
||||
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); |
||||
if (atData == null) { |
||||
LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress())); |
||||
return; |
||||
} |
||||
|
||||
if (tradeBotState.requiresTradeData) { |
||||
tradeData = DogecoinACCTv2.getInstance().populateTradeData(repository, atData); |
||||
if (tradeData == null) { |
||||
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress())); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
switch (tradeBotState) { |
||||
case BOB_WAITING_FOR_AT_CONFIRM: |
||||
handleBobWaitingForAtConfirm(repository, tradeBotData); |
||||
break; |
||||
|
||||
case BOB_WAITING_FOR_MESSAGE: |
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData); |
||||
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData); |
||||
break; |
||||
|
||||
case ALICE_WAITING_FOR_AT_LOCK: |
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData); |
||||
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData); |
||||
break; |
||||
|
||||
case BOB_WAITING_FOR_AT_REDEEM: |
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData); |
||||
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData); |
||||
break; |
||||
|
||||
case ALICE_DONE: |
||||
case BOB_DONE: |
||||
break; |
||||
|
||||
case ALICE_REFUNDING_A: |
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData); |
||||
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData); |
||||
break; |
||||
|
||||
case ALICE_REFUNDED: |
||||
case BOB_REFUNDED: |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Trade-bot is waiting for Bob's AT to deploy. |
||||
* <p> |
||||
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice. |
||||
*/ |
||||
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException { |
||||
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) { |
||||
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD) |
||||
return; |
||||
|
||||
// We've waited ages for AT to be confirmed into a block but something has gone awry.
|
||||
// After this long we assume transaction loss so give up with trade-bot entry too.
|
||||
tradeBotData.setState(State.BOB_REFUNDED.name()); |
||||
tradeBotData.setStateValue(State.BOB_REFUNDED.value); |
||||
tradeBotData.setTimestamp(NTP.getTime()); |
||||
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
|
||||
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey()); |
||||
repository.saveChanges(); |
||||
|
||||
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress())); |
||||
TradeBot.notifyStateChange(tradeBotData); |
||||
return; |
||||
} |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE, |
||||
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress())); |
||||
} |
||||
|
||||
/** |
||||
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info. |
||||
* <p> |
||||
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund, |
||||
* in which case trade-bot is done with this specific trade and finalizes on refunded state. |
||||
* <p> |
||||
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot. |
||||
* <p> |
||||
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance. |
||||
* <p> |
||||
* Assuming P2SH-A has at least expected Dogecoin balance, |
||||
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details. |
||||
* <p> |
||||
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice. |
||||
* <p> |
||||
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to |
||||
* extract secret-A needed to redeem Alice's P2SH. |
||||
* @throws ForeignBlockchainException |
||||
*/ |
||||
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData, |
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { |
||||
// If AT has finished then Bob likely cancelled his trade offer
|
||||
if (atData.getIsFinished()) { |
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED, |
||||
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress())); |
||||
return; |
||||
} |
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance(); |
||||
|
||||
String address = tradeBotData.getTradeNativeAddress(); |
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null); |
||||
|
||||
for (MessageTransactionData messageTransactionData : messageTransactionsData) { |
||||
if (messageTransactionData.isText()) |
||||
continue; |
||||
|
||||
// We're expecting: HASH160(secret-A), Alice's Dogecoin pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData(); |
||||
DogecoinACCTv2.OfferMessageData offerMessageData = DogecoinACCTv2.extractOfferMessageData(messageData); |
||||
if (offerMessageData == null) |
||||
continue; |
||||
|
||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDogecoinPKH; |
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA; |
||||
int lockTimeA = (int) offerMessageData.lockTimeA; |
||||
long messageTimestamp = messageTransactionData.getTimestamp(); |
||||
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA); |
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA); |
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA); |
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); |
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp); |
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee; |
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); |
||||
|
||||
switch (htlcStatusA) { |
||||
case UNFUNDED: |
||||
case FUNDING_IN_PROGRESS: |
||||
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||
continue; |
||||
|
||||
case REDEEM_IN_PROGRESS: |
||||
case REDEEMED: |
||||
// We've already redeemed this?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE, |
||||
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA)); |
||||
return; |
||||
|
||||
case REFUND_IN_PROGRESS: |
||||
case REFUNDED: |
||||
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||
continue; |
||||
|
||||
case FUNDED: |
||||
// Fall-through out of switch...
|
||||
break; |
||||
} |
||||
|
||||
// Good to go - send MESSAGE to AT
|
||||
|
||||
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey()); |
||||
|
||||
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||
byte[] outgoingMessageData = DogecoinACCTv2.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); |
||||
String messageRecipient = tradeBotData.getAtAddress(); |
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData); |
||||
if (!isMessageAlreadySent) { |
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); |
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false); |
||||
|
||||
outgoingMessageTransaction.computeNonce(); |
||||
outgoingMessageTransaction.sign(sender); |
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges(); |
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed(); |
||||
|
||||
if (result != ValidationResult.OK) { |
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM, |
||||
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress)); |
||||
|
||||
return; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only. |
||||
* <p> |
||||
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow |
||||
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else |
||||
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process. |
||||
* <p> |
||||
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct. |
||||
* <p> |
||||
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice. |
||||
* <p> |
||||
* In revealing a valid secret-A, Bob can then redeem the DOGE funds from P2SH-A. |
||||
* <p> |
||||
* @throws ForeignBlockchainException |
||||
*/ |
||||
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData, |
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { |
||||
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData)) |
||||
return; |
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance(); |
||||
int lockTimeA = tradeBotData.getLockTimeA(); |
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
if (NTP.getTime() >= lockTimeA * 1000L) { |
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); |
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA); |
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); |
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp); |
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; |
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); |
||||
|
||||
switch (htlcStatusA) { |
||||
case UNFUNDED: |
||||
case FUNDING_IN_PROGRESS: |
||||
case FUNDED: |
||||
break; |
||||
|
||||
case REDEEM_IN_PROGRESS: |
||||
case REDEEMED: |
||||
// Already redeemed?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, |
||||
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA)); |
||||
return; |
||||
|
||||
case REFUND_IN_PROGRESS: |
||||
case REFUNDED: |
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED, |
||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA)); |
||||
return; |
||||
|
||||
} |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A, |
||||
() -> atData.getIsFinished() |
||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA) |
||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA)); |
||||
|
||||
return; |
||||
} |
||||
|
||||
// We're waiting for AT to be in TRADE mode
|
||||
if (crossChainTradeData.mode != AcctMode.TRADING) |
||||
return; |
||||
|
||||
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
|
||||
|
||||
// Find our MESSAGE to AT from previous state
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(), |
||||
crossChainTradeData.qortalCreatorTradeAddress, null, null, null); |
||||
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) { |
||||
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress)); |
||||
return; |
||||
} |
||||
|
||||
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp(); |
||||
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(recipientMessageTimestamp, lockTimeA); |
||||
|
||||
// Our calculated refundTimeout should match AT's refundTimeout
|
||||
if (refundTimeout != crossChainTradeData.refundTimeout) { |
||||
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout)); |
||||
// We'll eventually refund
|
||||
return; |
||||
} |
||||
|
||||
// We're good to redeem AT
|
||||
|
||||
// Send 'redeem' MESSAGE to AT using both secret
|
||||
byte[] secretA = tradeBotData.getSecret(); |
||||
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
||||
byte[] messageData = DogecoinACCTv2.buildRedeemMessage(secretA, qortalReceivingAddress); |
||||
String messageRecipient = tradeBotData.getAtAddress(); |
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); |
||||
if (!isMessageAlreadySent) { |
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); |
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); |
||||
|
||||
messageTransaction.computeNonce(); |
||||
messageTransaction.sign(sender); |
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges(); |
||||
ValidationResult result = messageTransaction.importAsUnconfirmed(); |
||||
|
||||
if (result != ValidationResult.OK) { |
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name())); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, |
||||
() -> String.format("Redeeming AT %s. Funds should arrive at %s", |
||||
tradeBotData.getAtAddress(), qortalReceivingAddress)); |
||||
} |
||||
|
||||
/** |
||||
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DOGE funds from P2SH-A. |
||||
* <p> |
||||
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case, |
||||
* trade-bot is done with this specific trade and finalizes in refunded state. |
||||
* <p> |
||||
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DOGE funds from P2SH-A |
||||
* to Bob's 'foreign'/Dogecoin trade legacy-format address, as derived from trade private key. |
||||
* <p> |
||||
* (This could potentially be 'improved' to send DOGE to any address of Bob's choosing by changing the transaction output). |
||||
* <p> |
||||
* If trade-bot successfully broadcasts the transaction, then this specific trade is done. |
||||
* @throws ForeignBlockchainException |
||||
*/ |
||||
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData, |
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { |
||||
// AT should be 'finished' once Alice has redeemed QORT funds
|
||||
if (!atData.getIsFinished()) |
||||
// Not finished yet
|
||||
return; |
||||
|
||||
// If AT is REFUNDED or CANCELLED then something has gone wrong
|
||||
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) { |
||||
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DOGE
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED, |
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress())); |
||||
|
||||
return; |
||||
} |
||||
|
||||
byte[] secretA = DogecoinACCTv2.getInstance().findSecretA(repository, crossChainTradeData); |
||||
if (secretA == null) { |
||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress())); |
||||
return; |
||||
} |
||||
|
||||
// Use secret-A to redeem P2SH-A
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance(); |
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); |
||||
int lockTimeA = crossChainTradeData.lockTimeA; |
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA); |
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA); |
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); |
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp); |
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; |
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); |
||||
|
||||
switch (htlcStatusA) { |
||||
case UNFUNDED: |
||||
case FUNDING_IN_PROGRESS: |
||||
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
|
||||
return; |
||||
|
||||
case REDEEM_IN_PROGRESS: |
||||
case REDEEMED: |
||||
// Double-check that we have redeemed P2SH-A...
|
||||
break; |
||||
|
||||
case REFUND_IN_PROGRESS: |
||||
case REFUNDED: |
||||
// Wait for AT to auto-refund
|
||||
return; |
||||
|
||||
case FUNDED: { |
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); |
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); |
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA); |
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey, |
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); |
||||
|
||||
dogecoin.broadcastTransaction(p2shRedeemTransaction); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
String receivingAddress = dogecoin.pkhToAddress(receivingAccountInfo); |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE, |
||||
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress)); |
||||
} |
||||
|
||||
/** |
||||
* Trade-bot is attempting to refund P2SH-A. |
||||
* @throws ForeignBlockchainException |
||||
*/ |
||||
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData, |
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { |
||||
int lockTimeA = tradeBotData.getLockTimeA(); |
||||
|
||||
// We can't refund P2SH-A until lockTime-A has passed
|
||||
if (NTP.getTime() <= lockTimeA * 1000L) |
||||
return; |
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance(); |
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = dogecoin.getMedianBlockTime(); |
||||
if (medianBlockTime <= lockTimeA) |
||||
return; |
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); |
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA); |
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); |
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp); |
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; |
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA); |
||||
|
||||
switch (htlcStatusA) { |
||||
case UNFUNDED: |
||||
case FUNDING_IN_PROGRESS: |
||||
// Still waiting for P2SH-A to be funded...
|
||||
return; |
||||
|
||||
case REDEEM_IN_PROGRESS: |
||||
case REDEEMED: |
||||
// Too late!
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, |
||||
() -> String.format("P2SH-A %s already spent!", p2shAddressA)); |
||||
return; |
||||
|
||||
case REFUND_IN_PROGRESS: |
||||
case REFUNDED: |
||||
break; |
||||
|
||||
case FUNDED:{ |
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); |
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); |
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA); |
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); |
||||
Address receiving = Address.fromString(dogecoin.getNetworkParameters(), receiveAddress); |
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(dogecoin.getNetworkParameters(), refundAmount, refundKey, |
||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash()); |
||||
|
||||
dogecoin.broadcastTransaction(p2shRefundTransaction); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED, |
||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA)); |
||||
} |
||||
|
||||
/** |
||||
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else. |
||||
* <p> |
||||
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary. |
||||
* |
||||
* @throws DataException |
||||
* @throws ForeignBlockchainException |
||||
*/ |
||||
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData, |
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { |
||||
// This is OK
|
||||
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING) |
||||
return false; |
||||
|
||||
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress); |
||||
|
||||
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING) |
||||
if (isAtLockedToUs) { |
||||
// AT is trading with us - OK
|
||||
return false; |
||||
} else { |
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A, |
||||
() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress)); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) { |
||||
// We've redeemed already?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, |
||||
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress())); |
||||
} else { |
||||
// Any other state is not good, so start defensive refund
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A, |
||||
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress())); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) { |
||||
return (lockTimeA - tradeTimeout * 60) * 1000L; |
||||
} |
||||
|
||||
} |
@ -1,855 +0,0 @@
|
||||
package org.qortal.crosschain; |
||||
|
||||
import com.google.common.hash.HashCode; |
||||
import com.google.common.primitives.Bytes; |
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
import org.ciyam.at.*; |
||||
import org.qortal.account.Account; |
||||
import org.qortal.asset.Asset; |
||||
import org.qortal.at.QortalFunctionCode; |
||||
import org.qortal.crypto.Crypto; |
||||
import org.qortal.data.at.ATData; |
||||
import org.qortal.data.at.ATStateData; |
||||
import org.qortal.data.crosschain.CrossChainTradeData; |
||||
import org.qortal.data.transaction.MessageTransactionData; |
||||
import org.qortal.repository.DataException; |
||||
import org.qortal.repository.Repository; |
||||
import org.qortal.utils.Base58; |
||||
import org.qortal.utils.BitTwiddling; |
||||
|
||||
import java.nio.ByteBuffer; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset; |
||||
|
||||
/** |
||||
* Cross-chain trade AT |
||||
* |
||||
* <p> |
||||
* <ul> |
||||
* <li>Bob generates Dogecoin & Qortal 'trade' keys |
||||
* <ul> |
||||
* <li>private key required to sign P2SH redeem tx</li> |
||||
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li> |
||||
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Bob deploys Qortal AT |
||||
* <ul> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Alice finds Qortal AT and wants to trade |
||||
* <ul> |
||||
* <li>Alice generates Dogecoin & Qortal 'trade' keys</li> |
||||
* <li>Alice funds Dogecoin P2SH-A</li> |
||||
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing: |
||||
* <ul> |
||||
* <li>hash-of-secret-A</li> |
||||
* <li>her 'trade' Dogecoin PKH</li> |
||||
* </ul> |
||||
* </li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Bob receives "offer" MESSAGE |
||||
* <ul> |
||||
* <li>Checks Alice's P2SH-A</li> |
||||
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing: |
||||
* <ul> |
||||
* <li>Alice's trade Qortal address</li> |
||||
* <li>Alice's trade Dogecoin PKH</li> |
||||
* <li>hash-of-secret-A</li> |
||||
* </ul> |
||||
* </li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Alice checks Qortal AT to confirm it's locked to her |
||||
* <ul> |
||||
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing: |
||||
* <ul> |
||||
* <li>secret-A</li> |
||||
* <li>Qortal receiving address of her chosing</li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>AT's QORT funds are sent to Qortal receiving address</li> |
||||
* </ul> |
||||
* </li> |
||||
* <li>Bob checks AT, extracts secret-A |
||||
* <ul> |
||||
* <li>Bob redeems P2SH-A using his Dogecoin trade key and secret-A</li> |
||||
* <li>P2SH-A DOGE funds end up at Dogecoin address determined by redeem transaction output(s)</li> |
||||
* </ul> |
||||
* </li> |
||||
* </ul> |
||||
*/ |
||||
public class DogecoinACCTv2 implements ACCT { |
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2.class); |
||||
|
||||
public static final String NAME = DogecoinACCTv2.class.getSimpleName(); |
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("0eb49b0313ff3855a29d860c2a8203faa2ef62e28ea30459321f176079cfa3a6").asBytes(); // SHA256 of AT code bytes
|
||||
|
||||
public static final int SECRET_LENGTH = 32; |
||||
|
||||
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */ |
||||
private static final int MODE_VALUE_OFFSET = 61; |
||||
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */ |
||||
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE); |
||||
|
||||
public static class OfferMessageData { |
||||
public byte[] partnerDogecoinPKH; |
||||
public byte[] hashOfSecretA; |
||||
public long lockTimeA; |
||||
} |
||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDogecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/; |
||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/ |
||||
+ 24 /*partner's Dogecoin PKH (padded from 20 to 24)*/ |
||||
+ 8 /*AT trade timeout (minutes)*/ |
||||
+ 24 /*hash of secret-A (padded from 20 to 24)*/ |
||||
+ 8 /*lockTimeA*/; |
||||
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/; |
||||
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/; |
||||
|
||||
private static DogecoinACCTv2 instance; |
||||
|
||||
private DogecoinACCTv2() { |
||||
} |
||||
|
||||
public static synchronized DogecoinACCTv2 getInstance() { |
||||
if (instance == null) |
||||
instance = new DogecoinACCTv2(); |
||||
|
||||
return instance; |
||||
} |
||||
|
||||
@Override |
||||
public byte[] getCodeBytesHash() { |
||||
return CODE_BYTES_HASH; |
||||
} |
||||
|
||||
@Override |
||||
public int getModeByteOffset() { |
||||
return MODE_BYTE_OFFSET; |
||||
} |
||||
|
||||
@Override |
||||
public ForeignBlockchain getBlockchain() { |
||||
return Dogecoin.getInstance(); |
||||
} |
||||
|
||||
/** |
||||
* Returns Qortal AT creation bytes for cross-chain trading AT. |
||||
* <p> |
||||
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the |
||||
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator. |
||||
* |
||||
* @param creatorTradeAddress AT creator's trade Qortal address |
||||
* @param dogecoinPublicKeyHash 20-byte HASH160 of creator's trade Dogecoin public key |
||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT |
||||
* @param dogecoinAmount how much DOGE the AT creator is expecting to trade |
||||
* @param tradeTimeout suggested timeout for entire trade |
||||
*/ |
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] dogecoinPublicKeyHash, long qortAmount, long dogecoinAmount, int tradeTimeout) { |
||||
if (dogecoinPublicKeyHash.length != 20) |
||||
throw new IllegalArgumentException("Dogecoin public key hash should be 20 bytes"); |
||||
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0; |
||||
|
||||
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
||||
|
||||
final int addrCreatorTradeAddress1 = addrCounter++; |
||||
final int addrCreatorTradeAddress2 = addrCounter++; |
||||
final int addrCreatorTradeAddress3 = addrCounter++; |
||||
final int addrCreatorTradeAddress4 = addrCounter++; |
||||
|
||||
final int addrDogecoinPublicKeyHash = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrQortAmount = addrCounter++; |
||||
final int addrDogecoinAmount = addrCounter++; |
||||
final int addrTradeTimeout = addrCounter++; |
||||
|
||||
final int addrMessageTxnType = addrCounter++; |
||||
final int addrExpectedTradeMessageLength = addrCounter++; |
||||
final int addrExpectedRedeemMessageLength = addrCounter++; |
||||
|
||||
final int addrCreatorAddressPointer = addrCounter++; |
||||
final int addrQortalPartnerAddressPointer = addrCounter++; |
||||
final int addrMessageSenderPointer = addrCounter++; |
||||
|
||||
final int addrTradeMessagePartnerDogecoinPKHOffset = addrCounter++; |
||||
final int addrPartnerDogecoinPKHPointer = addrCounter++; |
||||
final int addrTradeMessageHashOfSecretAOffset = addrCounter++; |
||||
final int addrHashOfSecretAPointer = addrCounter++; |
||||
|
||||
final int addrRedeemMessageReceivingAddressOffset = addrCounter++; |
||||
|
||||
final int addrMessageDataPointer = addrCounter++; |
||||
final int addrMessageDataLength = addrCounter++; |
||||
|
||||
final int addrPartnerReceivingAddressPointer = addrCounter++; |
||||
|
||||
final int addrEndOfConstants = addrCounter; |
||||
|
||||
// Variables
|
||||
|
||||
final int addrCreatorAddress1 = addrCounter++; |
||||
final int addrCreatorAddress2 = addrCounter++; |
||||
final int addrCreatorAddress3 = addrCounter++; |
||||
final int addrCreatorAddress4 = addrCounter++; |
||||
|
||||
final int addrQortalPartnerAddress1 = addrCounter++; |
||||
final int addrQortalPartnerAddress2 = addrCounter++; |
||||
final int addrQortalPartnerAddress3 = addrCounter++; |
||||
final int addrQortalPartnerAddress4 = addrCounter++; |
||||
|
||||
final int addrLockTimeA = addrCounter++; |
||||
final int addrRefundTimeout = addrCounter++; |
||||
final int addrRefundTimestamp = addrCounter++; |
||||
final int addrLastTxnTimestamp = addrCounter++; |
||||
final int addrBlockTimestamp = addrCounter++; |
||||
final int addrTxnType = addrCounter++; |
||||
final int addrResult = addrCounter++; |
||||
|
||||
final int addrMessageSender1 = addrCounter++; |
||||
final int addrMessageSender2 = addrCounter++; |
||||
final int addrMessageSender3 = addrCounter++; |
||||
final int addrMessageSender4 = addrCounter++; |
||||
|
||||
final int addrMessageLength = addrCounter++; |
||||
|
||||
final int addrMessageData = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrHashOfSecretA = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrPartnerDogecoinPKH = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrPartnerReceivingAddress = addrCounter; |
||||
addrCounter += 4; |
||||
|
||||
final int addrMode = addrCounter++; |
||||
assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET); |
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); |
||||
|
||||
// AT creator's trade Qortal address, decoded from Base58
|
||||
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect"; |
||||
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress); |
||||
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0)); |
||||
|
||||
// Dogecoin public key hash
|
||||
assert dataByteBuffer.position() == addrDogecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrDogecoinPublicKeyHash incorrect"; |
||||
dataByteBuffer.put(Bytes.ensureCapacity(dogecoinPublicKeyHash, 32, 0)); |
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect"; |
||||
dataByteBuffer.putLong(qortAmount); |
||||
|
||||
// Expected Dogecoin amount
|
||||
assert dataByteBuffer.position() == addrDogecoinAmount * MachineState.VALUE_SIZE : "addrDogecoinAmount incorrect"; |
||||
dataByteBuffer.putLong(dogecoinAmount); |
||||
|
||||
// Suggested trade timeout (minutes)
|
||||
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; |
||||
dataByteBuffer.putLong(tradeTimeout); |
||||
|
||||
// We're only interested in MESSAGE transactions
|
||||
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect"; |
||||
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value); |
||||
|
||||
// Expected length of 'trade' MESSAGE data from AT creator
|
||||
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect"; |
||||
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH); |
||||
|
||||
// Expected length of 'redeem' MESSAGE data from trade partner
|
||||
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect"; |
||||
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH); |
||||
|
||||
// Index into data segment of AT creator's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect"; |
||||
dataByteBuffer.putLong(addrCreatorAddress1); |
||||
|
||||
// Index into data segment of partner's Qortal address, used by SET_B_IND
|
||||
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect"; |
||||
dataByteBuffer.putLong(addrQortalPartnerAddress1); |
||||
|
||||
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect"; |
||||
dataByteBuffer.putLong(addrMessageSender1); |
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting partner's Dogecoin PKH
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerDogecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDogecoinPKHOffset incorrect"; |
||||
dataByteBuffer.putLong(32L); |
||||
|
||||
// Index into data segment of partner's Dogecoin PKH, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerDogecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerDogecoinPKHPointer incorrect"; |
||||
dataByteBuffer.putLong(addrPartnerDogecoinPKH); |
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect"; |
||||
dataByteBuffer.putLong(64L); |
||||
|
||||
// Index into data segment to hash of secret A, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect"; |
||||
dataByteBuffer.putLong(addrHashOfSecretA); |
||||
|
||||
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
|
||||
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect"; |
||||
dataByteBuffer.putLong(32L); |
||||
|
||||
// Source location and length for hashing any passed secret
|
||||
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect"; |
||||
dataByteBuffer.putLong(addrMessageData); |
||||
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect"; |
||||
dataByteBuffer.putLong(32L); |
||||
|
||||
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect"; |
||||
dataByteBuffer.putLong(addrPartnerReceivingAddress); |
||||
|
||||
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants"; |
||||
|
||||
// Code labels
|
||||
Integer labelRefund = null; |
||||
|
||||
Integer labelTradeTxnLoop = null; |
||||
Integer labelCheckTradeTxn = null; |
||||
Integer labelCheckCancelTxn = null; |
||||
Integer labelNotTradeNorCancelTxn = null; |
||||
Integer labelCheckNonRefundTradeTxn = null; |
||||
Integer labelTradeTxnExtract = null; |
||||
Integer labelRedeemTxnLoop = null; |
||||
Integer labelCheckRedeemTxn = null; |
||||
Integer labelCheckRedeemTxnSender = null; |
||||
Integer labelPayout = null; |
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768); |
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) { |
||||
codeByteBuffer.clear(); |
||||
|
||||
try { |
||||
/* Initialization */ |
||||
|
||||
/* NOP - to ensure DOGECOIN ACCT is unique */ |
||||
codeByteBuffer.put(OpCode.NOP.compile()); |
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp)); |
||||
|
||||
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B)); |
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer)); |
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile()); |
||||
|
||||
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */ |
||||
|
||||
/* Transaction processing loop */ |
||||
labelTradeTxnLoop = codeByteBuffer.position(); |
||||
|
||||
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp)); |
||||
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult)); |
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn))); |
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile()); |
||||
|
||||
/* Check transaction */ |
||||
labelCheckTradeTxn = codeByteBuffer.position(); |
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp)); |
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType)); |
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop))); |
||||
|
||||
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */ |
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer)); |
||||
// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn))); |
||||
// Message sender's address matches AT creator's trade address so go process 'trade' message
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn)); |
||||
|
||||
/* Checking message sender for possible cancel message */ |
||||
labelCheckCancelTxn = codeByteBuffer.position(); |
||||
|
||||
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn))); |
||||
// Partner address is AT creator's address, so cancel offer and finish.
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value)); |
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile()); |
||||
|
||||
/* Not trade nor cancel message */ |
||||
labelNotTradeNorCancelTxn = codeByteBuffer.position(); |
||||
|
||||
// Loop to find another transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop)); |
||||
|
||||
/* Possible switch-to-trade-mode message */ |
||||
labelCheckNonRefundTradeTxn = codeByteBuffer.position(); |
||||
|
||||
// Check 'trade' message we received has expected number of message bytes
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength)); |
||||
// If message length matches, branch to info extraction code
|
||||
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract))); |
||||
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop)); |
||||
|
||||
/* Extracting info from 'trade' MESSAGE transaction */ |
||||
labelTradeTxnExtract = codeByteBuffer.position(); |
||||
|
||||
// Extract message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer)); |
||||
|
||||
// Extract trade partner's Dogecoin public key hash (PKH) from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDogecoinPKHOffset)); |
||||
// Store partner's Dogecoin PKH (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDogecoinPKHPointer)); |
||||
// Extract AT trade timeout (minutes) (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout)); |
||||
|
||||
// Grab next 32 bytes
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset)); |
||||
|
||||
// Extract hash-of-secret-A (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer)); |
||||
// Extract lockTime-A (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA)); |
||||
|
||||
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout)); |
||||
|
||||
/* We are in 'trade mode' */ |
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value)); |
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile()); |
||||
|
||||
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */ |
||||
|
||||
// Fetch current block 'timestamp'
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp)); |
||||
// If we're not past refund 'timestamp' then look for next transaction
|
||||
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
// We're past refund 'timestamp' so go refund everything back to AT creator
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund)); |
||||
|
||||
/* Transaction processing loop */ |
||||
labelRedeemTxnLoop = codeByteBuffer.position(); |
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp)); |
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult)); |
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn))); |
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile()); |
||||
|
||||
/* Check transaction */ |
||||
labelCheckRedeemTxn = codeByteBuffer.position(); |
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp)); |
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType)); |
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
|
||||
/* Check message payload length */ |
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength)); |
||||
// If message length matches, branch to sender checking code
|
||||
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender))); |
||||
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop)); |
||||
|
||||
/* Check transaction's sender */ |
||||
labelCheckRedeemTxnSender = codeByteBuffer.position(); |
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer)); |
||||
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop))); |
||||
|
||||
/* Check 'secret-A' in transaction's message */ |
||||
|
||||
// Extract secret-A from first 32 bytes of message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B)); |
||||
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer)); |
||||
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer)); |
||||
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
|
||||
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength)); |
||||
// If hashes don't match, addrResult will be zero so go find another transaction
|
||||
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout))); |
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop)); |
||||
|
||||
/* Success! Pay arranged amount to receiving address */ |
||||
labelPayout = codeByteBuffer.position(); |
||||
|
||||
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset)); |
||||
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer)); |
||||
// Pay AT's balance to receiving address
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount)); |
||||
// Set redeemed mode
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value)); |
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile()); |
||||
|
||||
// Fall-through to refunding any remaining balance back to AT creator
|
||||
|
||||
/* Refund balance back to AT creator */ |
||||
labelRefund = codeByteBuffer.position(); |
||||
|
||||
// Set refunded mode
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value)); |
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile()); |
||||
} catch (CompilationException e) { |
||||
throw new IllegalStateException("Unable to compile DOGE-QORT ACCT?", e); |
||||
} |
||||
} |
||||
|
||||
codeByteBuffer.flip(); |
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()]; |
||||
codeByteBuffer.get(codeBytes); |
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), DogecoinACCTv2.CODE_BYTES_HASH) |
||||
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes))); |
||||
|
||||
final short ciyamAtVersion = 2; |
||||
final short numCallStackPages = 0; |
||||
final short numUserStackPages = 0; |
||||
final long minActivationAmount = 0L; |
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); |
||||
} |
||||
|
||||
/** |
||||
* Returns CrossChainTradeData with useful info extracted from AT. |
||||
*/ |
||||
@Override |
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException { |
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress()); |
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData); |
||||
} |
||||
|
||||
/** |
||||
* Returns CrossChainTradeData with useful info extracted from AT. |
||||
*/ |
||||
@Override |
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException { |
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress()); |
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData); |
||||
} |
||||
|
||||
/** |
||||
* Returns CrossChainTradeData with useful info extracted from AT. |
||||
*/ |
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException { |
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress(); |
||||
|
||||
CrossChainTradeData tradeData = new CrossChainTradeData(); |
||||
|
||||
tradeData.foreignBlockchain = SupportedBlockchain.DOGECOIN.name(); |
||||
tradeData.acctName = NAME; |
||||
|
||||
tradeData.qortalAtAddress = atAddress; |
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey); |
||||
tradeData.creationTimestamp = creationTimestamp; |
||||
|
||||
Account atAccount = new Account(repository, atAddress); |
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT); |
||||
|
||||
byte[] stateData = atStateData.getStateData(); |
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData); |
||||
dataByteBuffer.position(MachineState.HEADER_LENGTH); |
||||
|
||||
/* Constants */ |
||||
|
||||
// Skip creator's trade address
|
||||
dataByteBuffer.get(addressBytes); |
||||
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length); |
||||
|
||||
// Creator's Dogecoin/foreign public key hash
|
||||
tradeData.creatorForeignPKH = new byte[20]; |
||||
dataByteBuffer.get(tradeData.creatorForeignPKH); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null; |
||||
|
||||
// Redeem payout
|
||||
tradeData.qortAmount = dataByteBuffer.getLong(); |
||||
|
||||
// Expected DOGE amount
|
||||
tradeData.expectedForeignAmount = dataByteBuffer.getLong(); |
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong(); |
||||
|
||||
// Skip MESSAGE transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip expected 'trade' message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip expected 'redeem' message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to creator's address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to partner's Qortal trade address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip 'trade' message data offset for partner's Dogecoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to partner's Dogecoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip 'trade' message data offset for hash-of-secret-A
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to hash-of-secret-A
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip 'redeem' message data offset for partner's Qortal receiving address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip message data length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip pointer to partner's receiving address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
/* End of constants / begin variables */ |
||||
|
||||
// Skip AT creator's address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); |
||||
|
||||
// Partner's trade address (if present)
|
||||
dataByteBuffer.get(addressBytes); |
||||
String qortalRecipient = Base58.encode(addressBytes); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length); |
||||
|
||||
// Potential lockTimeA (if in trade mode)
|
||||
int lockTimeA = (int) dataByteBuffer.getLong(); |
||||
|
||||
// AT refund timeout (probably only useful for debugging)
|
||||
int refundTimeout = (int) dataByteBuffer.getLong(); |
||||
|
||||
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
|
||||
long tradeRefundTimestamp = dataByteBuffer.getLong(); |
||||
|
||||
// Skip last transaction timestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip block timestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip temporary result
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip temporary message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); |
||||
|
||||
// Skip message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8); |
||||
|
||||
// Skip temporary message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); |
||||
|
||||
// Potential hash160 of secret A
|
||||
byte[] hashOfSecretA = new byte[20]; |
||||
dataByteBuffer.get(hashOfSecretA); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
||||
|
||||
// Potential partner's Dogecoin PKH
|
||||
byte[] partnerDogecoinPKH = new byte[20]; |
||||
dataByteBuffer.get(partnerDogecoinPKH); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDogecoinPKH.length); // skip to 32 bytes
|
||||
|
||||
// Partner's receiving address (if present)
|
||||
byte[] partnerReceivingAddress = new byte[25]; |
||||
dataByteBuffer.get(partnerReceivingAddress); |
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
|
||||
|
||||
// Trade AT's 'mode'
|
||||
long modeValue = dataByteBuffer.getLong(); |
||||
AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL)); |
||||
|
||||
/* End of variables */ |
||||
|
||||
if (mode != null && mode != AcctMode.OFFERING) { |
||||
tradeData.mode = mode; |
||||
tradeData.refundTimeout = refundTimeout; |
||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; |
||||
tradeData.qortalPartnerAddress = qortalRecipient; |
||||
tradeData.hashOfSecretA = hashOfSecretA; |
||||
tradeData.partnerForeignPKH = partnerDogecoinPKH; |
||||
tradeData.lockTimeA = lockTimeA; |
||||
|
||||
if (mode == AcctMode.REDEEMED) |
||||
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress); |
||||
} else { |
||||
tradeData.mode = AcctMode.OFFERING; |
||||
} |
||||
|
||||
tradeData.duplicateDeprecated(); |
||||
|
||||
return tradeData; |
||||
} |
||||
|
||||
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */ |
||||
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) { |
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); |
||||
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes); |
||||
} |
||||
|
||||
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */ |
||||
public static OfferMessageData extractOfferMessageData(byte[] messageData) { |
||||
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH) |
||||
return null; |
||||
|
||||
OfferMessageData offerMessageData = new OfferMessageData(); |
||||
offerMessageData.partnerDogecoinPKH = Arrays.copyOfRange(messageData, 0, 20); |
||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40); |
||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40); |
||||
|
||||
return offerMessageData; |
||||
} |
||||
|
||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */ |
||||
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) { |
||||
byte[] data = new byte[TRADE_MESSAGE_LENGTH]; |
||||
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress); |
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); |
||||
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout); |
||||
|
||||
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length); |
||||
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length); |
||||
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length); |
||||
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length); |
||||
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length); |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */ |
||||
@Override |
||||
public byte[] buildCancelMessage(String creatorQortalAddress) { |
||||
byte[] data = new byte[CANCEL_MESSAGE_LENGTH]; |
||||
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress); |
||||
|
||||
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length); |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */ |
||||
public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) { |
||||
byte[] data = new byte[REDEEM_MESSAGE_LENGTH]; |
||||
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress); |
||||
|
||||
System.arraycopy(secretA, 0, data, 0, secretA.length); |
||||
System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length); |
||||
|
||||
return data; |
||||
} |
||||
|
||||
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */ |
||||
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) { |
||||
// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
|
||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L); |
||||
} |
||||
|
||||
@Override |
||||
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException { |
||||
String atAddress = crossChainTradeData.qortalAtAddress; |
||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress; |
||||
|
||||
// We don't have partner's public key so we check every message to AT
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null); |
||||
if (messageTransactionsData == null) |
||||
return null; |
||||
|
||||
// Find 'redeem' message
|
||||
for (MessageTransactionData messageTransactionData : messageTransactionsData) { |
||||
// Check message payload type/encryption
|
||||
if (messageTransactionData.isText() || messageTransactionData.isEncrypted()) |
||||
continue; |
||||
|
||||
// Check message payload size
|
||||
byte[] messageData = messageTransactionData.getData(); |
||||
if (messageData.length != REDEEM_MESSAGE_LENGTH) |
||||
// Wrong payload length
|
||||
continue; |
||||
|
||||
// Check sender
|
||||
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress)) |
||||
// Wrong sender;
|
||||
continue; |
||||
|
||||
// Extract secretA
|
||||
byte[] secretA = new byte[32]; |
||||
System.arraycopy(messageData, 0, secretA, 0, secretA.length); |
||||
|
||||
byte[] hashOfSecretA = Crypto.hash160(secretA); |
||||
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA)) |
||||
continue; |
||||
|
||||
return secretA; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue