diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 4e32316c..91f23345 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -59,6 +59,38 @@ public class TradeBot { return instance; } + /** + * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for BTC. + *
+ * Generates: + *
+ * Trade-bot will wait for Bob's AT to be deployed before taking next step. + *
+ * @param repository + * @param tradeBotCreateRequest + * @return raw, unsigned DEPLOY_AT transaction + * @throws DataException + */ public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException { byte[] tradePrivateKey = generateTradePrivateKey(); byte[] secretB = generateSecret(); @@ -115,6 +147,44 @@ public class TradeBot { } } + /** + * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching BTC to an existing offer. + *
+ * Requires a chosen trade offer from Bob, passed by crossChainTradeData + * and access to a Bitcoin wallet via xprv58. + *
+ * The crossChainTradeData contains the current trade offer state + * as extracted from the AT's data segment. + *
+ * Access to a funded wallet is via a Bitcoin BIP32 hierarchical deterministic key, + * passed via xprv58. + * This key will be stored in your node's database + * 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). + *
+ * As an example, the xprv58 can be extract from a legacy, password-less
+ * Electrum wallet by going to the console tab and entering:
+ * wallet.keystore.xprv
+ * which should result in a base58 string starting with either 'xprv' (for Bitcoin main-net)
+ * or 'tprv' for (Bitcoin test-net).
+ *
+ * It is envisaged that the value in xprv58 will actually come from a Qortal-UI-managed wallet. + *
+ * If sufficient funds are available, this method will actually fund the P2SH-A + * with the Bitcoin amount expected by 'Bob'. + *
+ * If the Bitcoin transaction is successfully broadcast to the network then the trade-bot entry + * is saved to the repository and the cross-chain trading process commences. + *
+ * Trade-bot will wait for P2SH-A to confirm before taking next step. + *
+ * @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 static boolean startResponse(Repository repository, CrossChainTradeData crossChainTradeData, String xprv58) throws DataException { byte[] tradePrivateKey = generateTradePrivateKey(); byte[] secretA = generateSecret(); @@ -258,6 +328,11 @@ public class TradeBot { } } + /** + * Trade-bot is waiting for Bob's AT to deploy. + *
+ * 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())) return; @@ -269,6 +344,22 @@ public class TradeBot { LOGGER.info(() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress())); } + /** + * Trade-bot is waiting for Alice's P2SH-A to confirm. + *
+ * If P2SH-A is confirmed, then trade-bot's next step is to MESSAGE Bob's trade address with Alice's trade info. + *
+ * It is possible between broadcast and confirmation of P2SH-A funding transaction, that Bob has cancelled his trade offer. + * If this is detected then trade-bot's next step is to wait until P2SH-A can refund back to Alice. + *
+ * In normal operation, trade-bot send a zero-fee, PoW MESSAGE on Alice's behalf containing: + *
+ * 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. + *
+ * Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot. + *
+ * Details from Alice are used to derive P2SH-A address and this is checked for funding balance. + *
+ * 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. + *
+ * On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice. + *
+ * Trade-bot's next step is to wait for P2SH-B, which will allow Bob to reveal his secret-B, + * needed by Alice to progress her side of the trade. + */ private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException { // Fetch AT so we can determine trade start timestamp ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); @@ -426,6 +535,20 @@ public class TradeBot { } } + /** + * Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only. + *
+ * 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. + *
+ * Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct. + *
+ * If all is well, trade-bot then uses Bitcoin wallet to (token) fund P2SH-B. + *
+ * If P2SH-B funding transaction is successfully broadcast to the Bitcoin network, trade-bot's next + * step is to watch for Bob revealing secret-B by redeeming P2SH-B. + */ private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -527,6 +650,16 @@ public class TradeBot { tradeBotData.getAtAddress(), tradeBotData.getTradeNativeAddress(), p2shAddress)); } + /** + * Trade-bot is waiting for P2SH-B to funded. + *
+ * It's possible than Bob's AT has reached it's trading timeout and automatically refunded QORT back to Bob. + * In which case, trade-bot is done with this specific trade and finalizes on refunded state. + *
+ * Assuming P2SH-B is funded, trade-bot 'redeems' this P2SH using secret-B, thus revealing it to Alice. + *
+ * Trade-bot's next step is to wait for Alice to use secret-B, and her secret-A, to redeem Bob's AT. + */ private void handleBobWaitingForP2shB(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -583,6 +716,22 @@ public class TradeBot { LOGGER.info(() -> String.format("P2SH-B %s redeemed (exposing secret-B). Watching AT %s for secret-A", p2shAddress, tradeBotData.getAtAddress())); } + /** + * Trade-bot is waiting for Bob to redeem P2SH-B thus revealing secret-B to Alice. + *
+ * It's possible that this process has taken so long that we've reached P2SH-B's locktime. + * In which case, trade-bot switches to begin the refund process. + *
+ * If trade-bot can extract a valid secret-B from the spend of P2SH-B, then it creates a + * zero-fee, PoW MESSAGE to send to Bob's AT, including both secret-B and also Alice's secret-A. + *
+ * Both secrets are needed to release the QORT funds from Bob's AT to Alice's 'native'/Qortal + * trade address. + *
+ * In revealing a valid secret-A, Bob can then redeem the BTC funds from P2SH-A. + *
+ * If trade-bot successfully broadcasts the MESSAGE transaction, then this specific trade is done. + */ private void handleAliceWatchingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -645,6 +794,19 @@ public class TradeBot { p2shAddress, tradeBotData.getAtAddress(), receiveAddress)); } + /** + * 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. + *
+ * 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. + *
+ * 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. + *
+ * (This could potentially be 'improved' to send BTC to any address of Bob's choosing by changing the transaction output). + *
+ * If trade-bot successfully broadcasts the transaction, then this specific trade is done. + */ private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -702,6 +864,13 @@ public class TradeBot { LOGGER.info(() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receiveAddress)); } + /** + * Trade-bot is attempting to refund P2SH-B. + *
+ * We could potentially skip this step as P2SH-B is only funded with a token amount to cover the mining fee should Bob redeem P2SH-B. + *
+ * Upon successful broadcast of P2SH-B refunding transaction, trade-bot's next step is to begin refunding of P2SH-A. + */ private void handleAliceRefundingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -736,6 +905,7 @@ public class TradeBot { LOGGER.info(() -> String.format("Refunded P2SH-B %s. Waiting for LockTime-A", p2shAddress)); } + /** Trade-bot is attempting to refund P2SH-A. */ private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 3910bfa4..cb87ca0f 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -77,11 +77,13 @@ import com.google.common.primitives.Bytes; *