forked from Qortal/qortal
Add Bitcoin ACCTv3
This provides support for restoring BTC in the Trade Portal.
This commit is contained in:
parent
acce81cdcd
commit
9f9a74809e
@ -0,0 +1,885 @@
|
|||||||
|
npackage 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 BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3TradeBot.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 BitcoinACCTv3TradeBot 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 BitcoinACCTv3TradeBot() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized BitcoinACCTv3TradeBot getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new BitcoinACCTv3TradeBot();
|
||||||
|
|
||||||
|
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 BTC.
|
||||||
|
* <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 Bitcoin) 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'/Bitcoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||||
|
* <li>QORT amount on offer by Bob</li>
|
||||||
|
* <li>BTC 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 Bitcoin receiving address into public key hash (we only support P2PKH at this time)
|
||||||
|
Address bitcoinReceivingAddress;
|
||||||
|
try {
|
||||||
|
bitcoinReceivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||||
|
} catch (AddressFormatException e) {
|
||||||
|
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
}
|
||||||
|
if (bitcoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||||
|
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
|
||||||
|
byte[] bitcoinReceivingAccountInfo = bitcoinReceivingAddress.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/BTC ACCT";
|
||||||
|
String description = "QORT/BTC cross-chain trade";
|
||||||
|
String aTType = "ACCT";
|
||||||
|
String tags = "ACCT QORT BTC";
|
||||||
|
byte[] creationBytes = BitcoinACCTv3.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, BitcoinACCTv3.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.BITCOIN.name(),
|
||||||
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
tradeBotCreateRequest.foreignAmount, null, null, null, bitcoinReceivingAccountInfo);
|
||||||
|
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||||
|
|
||||||
|
// Attempt to backup the trade bot data
|
||||||
|
TradeBot.backupTradeBotData(repository, null);
|
||||||
|
|
||||||
|
// 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 BTC to an existing offer.
|
||||||
|
* <p>
|
||||||
|
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||||
|
* and access to a Bitcoin 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 Bitcoin 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 Bitcoin main-net)
|
||||||
|
* or 'tprv' for (Bitcoin 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 Bitcoin amount expected by 'Bob'.
|
||||||
|
* <p>
|
||||||
|
* If the Bitcoin 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 Bitcoin 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, BitcoinACCTv3.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.BITCOIN.name(),
|
||||||
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
||||||
|
|
||||||
|
// Attempt to backup the trade bot data
|
||||||
|
// Include tradeBotData as an additional parameter, since it's not in the repository yet
|
||||||
|
TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
|
||||||
|
|
||||||
|
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||||
|
long p2shFee;
|
||||||
|
try {
|
||||||
|
p2shFee = Bitcoin.getInstance().getP2shFee(now);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
LOGGER.debug("Couldn't estimate Bitcoin 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 = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||||
|
|
||||||
|
// Build transaction for funding P2SH-A
|
||||||
|
Transaction p2shFundingTransaction = Bitcoin.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 {
|
||||||
|
Bitcoin.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 = BitcoinACCTv3.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:
|
||||||
|
case ALICE_REFUNDING_A:
|
||||||
|
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 = BitcoinACCTv3.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 Bitcoin 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitcoin bitcoin = Bitcoin.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 Bitcoin pubkeyhash and lockTime-A
|
||||||
|
byte[] messageData = messageTransactionData.getData();
|
||||||
|
BitcoinACCTv3.OfferMessageData offerMessageData = BitcoinACCTv3.extractOfferMessageData(messageData);
|
||||||
|
if (offerMessageData == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
|
||||||
|
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||||
|
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||||
|
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||||
|
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Determine P2SH-A address and confirm funded
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||||
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.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 = BitcoinACCTv3.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 BTC 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;
|
||||||
|
|
||||||
|
Bitcoin bitcoin = Bitcoin.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 = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.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 = BitcoinACCTv3.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 = BitcoinACCTv3.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 BTC 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 BTC funds from P2SH-A
|
||||||
|
* to Bob's 'foreign'/Bitcoin trade legacy-format address, as derived from trade private key.
|
||||||
|
* <p>
|
||||||
|
* (This could potentially be 'improved' to send BTC 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 BTC
|
||||||
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
|
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] secretA = BitcoinACCTv3.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
|
||||||
|
|
||||||
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
|
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
|
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.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 = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||||
|
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||||
|
|
||||||
|
bitcoin.broadcastTransaction(p2shRedeemTransaction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String receivingAddress = bitcoin.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;
|
||||||
|
|
||||||
|
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||||
|
|
||||||
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
|
int medianBlockTime = bitcoin.getMedianBlockTime();
|
||||||
|
if (medianBlockTime <= lockTimeA)
|
||||||
|
return;
|
||||||
|
|
||||||
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
|
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||||
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.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 = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
// Determine receive address for refund
|
||||||
|
String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||||
|
Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress);
|
||||||
|
|
||||||
|
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
|
||||||
|
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||||
|
|
||||||
|
bitcoin.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -94,6 +94,7 @@ public class TradeBot implements Listener {
|
|||||||
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
|
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
|
||||||
static {
|
static {
|
||||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||||
|
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
|
||||||
|
858
src/main/java/org/qortal/crosschain/BitcoinACCTv3.java
Normal file
858
src/main/java/org/qortal/crosschain/BitcoinACCTv3.java
Normal file
@ -0,0 +1,858 @@
|
|||||||
|
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 Bitcoin & 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 Bitcoin & Qortal 'trade' keys</li>
|
||||||
|
* <li>Alice funds Bitcoin 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' Bitcoin 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 Bitcoin 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 Bitcoin trade key and secret-A</li>
|
||||||
|
* <li>P2SH-A BTC funds end up at Bitcoin address determined by redeem transaction output(s)</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class BitcoinACCTv3 implements ACCT {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3.class);
|
||||||
|
|
||||||
|
public static final String NAME = BitcoinACCTv3.class.getSimpleName();
|
||||||
|
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("676fb9350708dafa054eb0262d655039e393c1eb4918ec582f8d45524c9b4860").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[] partnerBitcoinPKH;
|
||||||
|
public byte[] hashOfSecretA;
|
||||||
|
public long lockTimeA;
|
||||||
|
}
|
||||||
|
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerBitcoinPKH*/ + 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 Bitcoin 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 BitcoinACCTv3 instance;
|
||||||
|
|
||||||
|
private BitcoinACCTv3() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized BitcoinACCTv3 getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new BitcoinACCTv3();
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getCodeBytesHash() {
|
||||||
|
return CODE_BYTES_HASH;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getModeByteOffset() {
|
||||||
|
return MODE_BYTE_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ForeignBlockchain getBlockchain() {
|
||||||
|
return Bitcoin.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 bitcoinPublicKeyHash 20-byte HASH160 of creator's trade Bitcoin public key
|
||||||
|
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||||
|
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
|
||||||
|
* @param tradeTimeout suggested timeout for entire trade
|
||||||
|
*/
|
||||||
|
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, long qortAmount, long bitcoinAmount, int tradeTimeout) {
|
||||||
|
if (bitcoinPublicKeyHash.length != 20)
|
||||||
|
throw new IllegalArgumentException("Bitcoin 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 addrBitcoinPublicKeyHash = addrCounter;
|
||||||
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrQortAmount = addrCounter++;
|
||||||
|
final int addrBitcoinAmount = 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 addrTradeMessagePartnerBitcoinPKHOffset = addrCounter++;
|
||||||
|
final int addrPartnerBitcoinPKHPointer = 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 addrPartnerBitcoinPKH = 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));
|
||||||
|
|
||||||
|
// Bitcoin public key hash
|
||||||
|
assert dataByteBuffer.position() == addrBitcoinPublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinPublicKeyHash incorrect";
|
||||||
|
dataByteBuffer.put(Bytes.ensureCapacity(bitcoinPublicKeyHash, 32, 0));
|
||||||
|
|
||||||
|
// Redeem Qort amount
|
||||||
|
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||||
|
dataByteBuffer.putLong(qortAmount);
|
||||||
|
|
||||||
|
// Expected Bitcoin amount
|
||||||
|
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
|
||||||
|
dataByteBuffer.putLong(bitcoinAmount);
|
||||||
|
|
||||||
|
// 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 Bitcoin PKH
|
||||||
|
assert dataByteBuffer.position() == addrTradeMessagePartnerBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerBitcoinPKHOffset incorrect";
|
||||||
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
|
// Index into data segment of partner's Bitcoin PKH, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrPartnerBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerBitcoinPKHPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrPartnerBitcoinPKH);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
/* Sleep until message arrives */
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
|
||||||
|
|
||||||
|
// 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 Bitcoin 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, addrTradeMessagePartnerBitcoinPKHOffset));
|
||||||
|
// Store partner's Bitcoin PKH (we only really use values from B1-B3)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerBitcoinPKHPointer));
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
/* NOP - to ensure BITCOIN ACCT is unique */
|
||||||
|
codeByteBuffer.put(OpCode.NOP.compile());
|
||||||
|
|
||||||
|
// 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 BTC-QORT ACCT?", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeByteBuffer.flip();
|
||||||
|
|
||||||
|
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||||
|
codeByteBuffer.get(codeBytes);
|
||||||
|
|
||||||
|
assert Arrays.equals(Crypto.digest(codeBytes), BitcoinACCTv3.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.BITCOIN.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 Bitcoin/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 BTC 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 Bitcoin PKH
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to partner's Bitcoin 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 Bitcoin PKH
|
||||||
|
byte[] partnerBitcoinPKH = new byte[20];
|
||||||
|
dataByteBuffer.get(partnerBitcoinPKH);
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerBitcoinPKH.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 = partnerBitcoinPKH;
|
||||||
|
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.partnerBitcoinPKH = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -13,8 +13,8 @@ import org.qortal.utils.Triple;
|
|||||||
public enum SupportedBlockchain {
|
public enum SupportedBlockchain {
|
||||||
|
|
||||||
BITCOIN(Arrays.asList(
|
BITCOIN(Arrays.asList(
|
||||||
Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance)
|
Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance),
|
||||||
// Could add improved BitcoinACCTv2 here in the future
|
Triple.valueOf(BitcoinACCTv3.NAME, BitcoinACCTv3.CODE_BYTES_HASH, BitcoinACCTv3::getInstance)
|
||||||
)) {
|
)) {
|
||||||
@Override
|
@Override
|
||||||
public ForeignBlockchain getInstance() {
|
public ForeignBlockchain getInstance() {
|
||||||
@ -23,7 +23,7 @@ public enum SupportedBlockchain {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ACCT getLatestAcct() {
|
public ACCT getLatestAcct() {
|
||||||
return BitcoinACCTv1.getInstance();
|
return BitcoinACCTv3.getInstance();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -0,0 +1,769 @@
|
|||||||
|
package org.qortal.test.crosschain.bitcoinv3;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
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.AcctMode;
|
||||||
|
import org.qortal.crosschain.BitcoinACCTv3;
|
||||||
|
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 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 static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class BitcoinACCTv3Tests 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[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
|
||||||
|
public static final int tradeTimeout = 20; // blocks
|
||||||
|
public static final long redeemAmount = 80_40200000L;
|
||||||
|
public static final long fundingAmount = 123_45600000L;
|
||||||
|
public static final long bitcoinAmount = 864200L; // 0.00864200 BTC
|
||||||
|
|
||||||
|
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 = BitcoinACCTv3.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, 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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.getInstance().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 Bitcoin PKH was extracted correctly
|
||||||
|
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
|
||||||
|
|
||||||
|
// 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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT BUT NOT FROM AT CREATOR
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.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 = BitcoinACCTv3.getInstance().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 = BitcoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
|
||||||
|
|
||||||
|
// Send trade info to AT
|
||||||
|
byte[] messageData = BitcoinACCTv3.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, 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 = BitcoinACCTv3.getInstance().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, BitcoinACCTv3.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 = BitcoinACCTv3.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, redeemAmount, bitcoinAmount, 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-BTC cross-chain trade";
|
||||||
|
String description = String.format("Qortal-Bitcoin cross-chain trade");
|
||||||
|
String atType = "ACCT";
|
||||||
|
String tags = "QORT-BTC 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 = BitcoinACCTv3.getInstance().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 Bitcoin: %s BTC,\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.expectedForeignAmount),
|
||||||
|
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"
|
||||||
|
+ "\tBitcoin 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user