WIP: TradeBot - added refunding code

This commit is contained in:
catbref 2020-07-13 11:14:45 +01:00
parent 579645d6b7
commit f9b726a75d
4 changed files with 250 additions and 58 deletions

View File

@ -1029,8 +1029,16 @@ public class CrossChainResource {
try (final Repository repository = RepositoryManager.getRepository()) {
TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey);
if (tradeBotData.getState() != TradeBotData.State.ALICE_DONE && tradeBotData.getState() != TradeBotData.State.BOB_DONE)
switch (tradeBotData.getState()) {
case ALICE_DONE:
case BOB_DONE:
case ALICE_REFUNDED:
case BOB_REFUNDED:
break;
default:
return "false";
}
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
repository.saveChanges();

View File

@ -20,6 +20,7 @@ import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
@ -104,6 +105,8 @@ public class TradeBot {
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("Built AT %s. Waiting for deployment", atAddress));
// Return to user for signing and broadcast as we don't have their Qortal private key
try {
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
@ -124,7 +127,7 @@ public class TradeBot {
byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
// We need to generate lockTimeA: halfway of refundTimeout from now
// We need to generate lockTime-A: halfway of refundTimeout from now
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A,
@ -134,30 +137,32 @@ public class TradeBot {
tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedBitcoin, xprv58, null, lockTimeA);
// Check we have enough funds via xprv58 to fund both P2SH to cover expectedBitcoin
// Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin
String tradeForeignAddress = BTC.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
long totalFundsRequired = crossChainTradeData.expectedBitcoin + FEE_AMOUNT /* P2SH-a */ + FEE_AMOUNT /* P2SH-b */;
long totalFundsRequired = crossChainTradeData.expectedBitcoin + FEE_AMOUNT /* P2SH-A */ + FEE_AMOUNT /* P2SH-B */;
Transaction fundingCheckTransaction = BTC.getInstance().buildSpend(xprv58, tradeForeignAddress, totalFundsRequired);
if (fundingCheckTransaction == null)
return false;
// P2SH_a to be funded
// P2SH-A to be funded
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
// Fund P2SH-a
// Fund P2SH-A
Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, crossChainTradeData.expectedBitcoin + FEE_AMOUNT);
if (!BTC.getInstance().broadcastTransaction(p2shFundingTransaction)) {
// We couldn't fund P2SH-a at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-a funding transaction?"));
// We couldn't fund P2SH-A at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-A funding transaction?"));
return false;
}
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("Funding P2SH-A %s. Waiting for confirmation", p2shAddress));
return true;
}
@ -230,6 +235,18 @@ public class TradeBot {
case BOB_DONE:
break;
case ALICE_REFUNDING_B:
handleAliceRefundingP2shB(repository, tradeBotData);
break;
case ALICE_REFUNDING_A:
handleAliceRefundingP2shA(repository, tradeBotData);
break;
case ALICE_REFUNDED:
case BOB_REFUNDED:
break;
default:
LOGGER.warn(() -> String.format("Unhandled trade-bot state %s", tradeBotData.getState().name()));
}
@ -248,12 +265,14 @@ public class TradeBot {
tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_MESSAGE);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
}
private void handleAliceWaitingForP2shA(Repository repository, TradeBotData tradeBotData) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
@ -261,14 +280,28 @@ public class TradeBot {
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Long balance = BTC.getInstance().getBalance(p2shAddress);
if (balance == null || balance < crossChainTradeData.expectedBitcoin) {
if (balance != null && balance > 0)
LOGGER.debug(() -> String.format("P2SH-a balance %s lower than expected %s", BTC.format(balance), BTC.format(crossChainTradeData.expectedBitcoin)));
// If AT has finished then maybe Bob cancelled his trade offer
if (atData.getIsFinished()) {
// No point sending MESSAGE - might as well wait for refund
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_A);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress));
return;
}
Long balance = BTC.getInstance().getBalance(p2shAddress);
if (balance == null || balance < crossChainTradeData.expectedBitcoin) {
if (balance != null && balance > 0)
LOGGER.debug(() -> String.format("P2SH-A balance %s lower than expected %s", BTC.format(balance), BTC.format(crossChainTradeData.expectedBitcoin)));
return;
}
// P2SH-A funding confirmed
// Attempt to send MESSAGE to Bob's Qortal trade address
byte[] messageData = BTCACCT.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
@ -283,20 +316,34 @@ public class TradeBot {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT '%s': %s", messageTransaction.getRecipient(), result.name()));
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageTransaction.getRecipient(), result.name()));
return;
}
tradeBotData.setState(TradeBotData.State.ALICE_WAITING_FOR_AT_LOCK);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("P2SH-A %s funding confirmed. Messaged %s. Waiting for AT %s to lock to us",
p2shAddress, crossChainTradeData.qortalCreatorTradeAddress, tradeBotData.getAtAddress()));
}
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());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
// If AT has finished then Bob likely cancelled his trade offer
if (atData.getIsFinished()) {
tradeBotData.setState(TradeBotData.State.BOB_REFUNDED);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
return;
}
@ -320,7 +367,7 @@ public class TradeBot {
if (messageTransactionData.isText())
continue;
// We're expecting: HASH160(secret) + Alice's Bitcoin pubkeyhash
// We're expecting: HASH160(secret-A), Alice's Bitcoin pubkeyhash and lockTime-A
byte[] messageData = messageTransactionData.getData();
BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData);
if (offerMessageData == null)
@ -356,13 +403,19 @@ public class TradeBot {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT '%s': %s", outgoingMessageTransaction.getRecipient(), result.name()));
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", outgoingMessageTransaction.getRecipient(), result.name()));
return;
}
tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_P2SH_B);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
byte[] redeemScriptBytes = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeB, tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret());
String p2shBAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
LOGGER.info(() -> String.format("Locked AT %s to %s. Waiting for P2SH-B %s", tradeBotData.getAtAddress(), aliceNativeAddress, p2shBAddress));
return;
}
@ -374,15 +427,30 @@ public class TradeBot {
}
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData) throws DataException {
// XXX REFUND CHECK
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// Refund P2SH-A if AT finished (i.e. Bob cancelled trade) or we've passed lockTime-A
if (atData.getIsFinished() || NTP.getTime() >= tradeBotData.getLockTimeA()) {
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_A);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
if (atData.getIsFinished())
LOGGER.info(() -> String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress));
else
LOGGER.info(() -> String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddress));
return;
}
// We're waiting for AT to be in TRADE mode
if (crossChainTradeData.mode != CrossChainTradeData.Mode.TRADE)
return;
@ -390,12 +458,17 @@ public class TradeBot {
// We're expecting AT to be locked to our native trade address
if (!crossChainTradeData.qortalRecipient.equals(tradeBotData.getTradeNativeAddress())) {
// AT locked to different address! We shouldn't continue but wait and refund.
LOGGER.warn(() -> String.format("Trade AT '%s' locked to '%s', not us ('%s')",
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
LOGGER.warn(() -> String.format("AT %s locked to %s, not us (%s). Refunding %s - aborting trade",
tradeBotData.getAtAddress(),
crossChainTradeData.qortalRecipient,
tradeBotData.getTradeNativeAddress()));
tradeBotData.getTradeNativeAddress(),
p2shAddress));
// There's no P2SH-b at this point, so jump straight to refunding P2SH-a
// There's no P2SH-B at this point, so jump straight to refunding P2SH-A
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_A);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
@ -403,12 +476,12 @@ public class TradeBot {
return;
}
// Alice needs to fund P2SH-b here
// Alice needs to fund P2SH-B here
// Find our MESSAGE to AT from previous state
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
if (messageTransactionsData == null) {
LOGGER.warn(() -> String.format("Unable to fetch messages to trade AT '%s' from repository", crossChainTradeData.qortalCreatorTradeAddress));
LOGGER.warn(() -> String.format("Unable to fetch messages to trade AT %s from repository", crossChainTradeData.qortalCreatorTradeAddress));
return;
}
@ -421,16 +494,16 @@ public class TradeBot {
}
if (recipientMessageTimestamp == null) {
LOGGER.warn(() -> String.format("Unable to find our message to trade creator '%s'?", crossChainTradeData.qortalCreatorTradeAddress));
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
return;
}
int lockTimeA = tradeBotData.getLockTimeA();
int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTimestamp, lockTimeA);
// Our calculated lockTimeB should match AT's calculated lockTimeB
// Our calculated lockTime-B should match AT's calculated lockTime-B
if (lockTimeB != crossChainTradeData.lockTimeB) {
LOGGER.debug(() -> String.format("Trade AT lockTimeB '%d' doesn't match our lockTimeB '%d'", crossChainTradeData.lockTimeB, lockTimeB));
LOGGER.debug(() -> String.format("Trade AT lockTime-B '%d' doesn't match our lockTime-B '%d'", crossChainTradeData.lockTimeB, lockTimeB));
// We'll eventually refund
return;
}
@ -440,27 +513,39 @@ public class TradeBot {
Transaction p2shFundingTransaction = BTC.getInstance().buildSpend(tradeBotData.getXprv58(), p2shAddress, FEE_AMOUNT);
if (!BTC.getInstance().broadcastTransaction(p2shFundingTransaction)) {
// We couldn't fund P2SH-b at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-b funding transaction?"));
// We couldn't fund P2SH-B at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B funding transaction?"));
return;
}
// P2SH-b funded, now we wait for Bob to redeem it
// P2SH-B funded, now we wait for Bob to redeem it
tradeBotData.setState(TradeBotData.State.ALICE_WATCH_P2SH_B);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s locked to us (%s). P2SH-B %s funded. Watching P2SH-B for secret-B",
tradeBotData.getAtAddress(), tradeBotData.getTradeNativeAddress(), p2shAddress));
}
private void handleBobWaitingForP2shB(Repository repository, TradeBotData tradeBotData) throws DataException {
// XXX REFUND CHECK
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// If we've passed AT refund timestamp then AT will have finished after auto-refunding
if (atData.getIsFinished()) {
tradeBotData.setState(TradeBotData.State.BOB_REFUNDED);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
return;
}
// It's possible AT hasn't processed our previous MESSAGE yet and so lockTimeB won't be set
if (crossChainTradeData.lockTimeB == null)
// AT yet to process MESSAGE
@ -472,36 +557,36 @@ public class TradeBot {
Long balance = BTC.getInstance().getBalance(p2shAddress);
if (balance == null || balance < FEE_AMOUNT) {
if (balance != null && balance > 0)
LOGGER.debug(() -> String.format("P2SH-b balance %s lower than expected %s", BTC.format(balance), BTC.format(FEE_AMOUNT)));
LOGGER.debug(() -> String.format("P2SH-B balance %s lower than expected %s", BTC.format(balance), BTC.format(FEE_AMOUNT)));
return;
}
// Redeem P2SH-b using secret-b
Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-a
// Redeem P2SH-B using secret-B
Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, tradeBotData.getSecret());
if (!BTC.getInstance().broadcastTransaction(p2shRedeemTransaction)) {
// We couldn't redeem P2SH-b at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-b redeeming transaction?"));
// We couldn't redeem P2SH-B at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B redeeming transaction?"));
return;
}
// P2SH-b redeemed, now we wait for Alice to use secret to redeem AT
// P2SH-B redeemed, now we wait for Alice to use secret-A to redeem AT
tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_AT_REDEEM);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("P2SH-B %s redeemed (exposing secret-B). Watching AT %s for secret-A", p2shAddress, tradeBotData.getAtAddress()));
}
private void handleAliceWatchingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException {
// XXX REFUND CHECK
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
@ -509,9 +594,20 @@ public class TradeBot {
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
// Refund P2SH-B if we've passed lockTime-B
if (NTP.getTime() >= crossChainTradeData.lockTimeB) {
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_B);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("LockTime-B reached, refunding P2SH-B %s - aborting trade", p2shAddress));
return;
}
List<byte[]> p2shTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
if (p2shTransactions == null) {
LOGGER.debug(() -> String.format("Unable to fetch transactions relating to '%s'", p2shAddress));
LOGGER.debug(() -> String.format("Unable to fetch transactions relating to %s", p2shAddress));
return;
}
@ -530,26 +626,29 @@ public class TradeBot {
messageTransaction.computeNonce();
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
// 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", messageTransaction.getRecipient(), result.name()));
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageTransaction.getRecipient(), result.name()));
return;
}
tradeBotData.setState(TradeBotData.State.ALICE_DONE);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
String receiveAddress = tradeBotData.getTradeNativeAddress();
LOGGER.info(() -> String.format("P2SH-B %s redeemed, using secrets to redeem AT %s. Funds should arrive at %s",
p2shAddress, tradeBotData.getAtAddress(), receiveAddress));
}
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData) throws DataException {
// XXX REFUND CHECK
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
@ -559,13 +658,25 @@ public class TradeBot {
// Not finished yet
return;
byte[] secretA = BTCACCT.findSecretA(repository, crossChainTradeData);
if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-a from redeem message to AT '%s'?", tradeBotData.getAtAddress()));
// If AT's balance is zero, then it's auto-refunded so we're done
AccountBalanceData atBalanceData = repository.getAccountRepository().getBalance(tradeBotData.getAtAddress(), Asset.QORT);
if (atBalanceData == null || atBalanceData.getBalance() == 0L) {
tradeBotData.setState(TradeBotData.State.BOB_REFUNDED);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
return;
}
// Use secretA to redeem P2SH-a
byte[] secretA = BTCACCT.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
byte[] redeemScriptBytes = BTCP2SH.buildScript(crossChainTradeData.recipientBitcoinPKH, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretA);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
@ -577,14 +688,86 @@ public class TradeBot {
Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA);
if (!BTC.getInstance().broadcastTransaction(p2shRedeemTransaction)) {
// We couldn't redeem P2SH-a at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-a redeeming transaction?"));
// We couldn't redeem P2SH-A at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-A redeeming transaction?"));
return;
}
tradeBotData.setState(TradeBotData.State.BOB_DONE);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
String receiveAddress = BTC.getInstance().pkhToAddress(tradeBotData.getTradeForeignPublicKeyHash());
LOGGER.info(() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receiveAddress));
}
private void handleAliceRefundingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// We can't refund P2SH-B until lockTime-B has passed
if (NTP.getTime() <= crossChainTradeData.lockTimeB)
return;
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorBitcoinPKH, crossChainTradeData.hashOfSecretB);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Coin refundAmount = Coin.ZERO;
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA());
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
// We couldn't refund P2SH-B at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B refund transaction?"));
return;
}
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDING_A);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("Refunded P2SH-B %s. Waiting for LockTime-A", p2shAddress));
}
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.warn(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData);
// We can't refund P2SH-A until lockTime-A has passed
if (NTP.getTime() <= tradeBotData.getLockTimeA())
return;
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret());
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes);
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA());
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
// We couldn't refund P2SH-A at this time
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-A refund transaction?"));
return;
}
tradeBotData.setState(TradeBotData.State.ALICE_REFUNDED);
repository.getCrossChainRepository().save(tradeBotData);
repository.saveChanges();
LOGGER.info(() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddress));
}
}

View File

@ -117,6 +117,7 @@ public class BTC {
return format(Coin.valueOf(amount));
}
/** Returns P2PKH Bitcoin address using passed public key hash. */
public String pkhToAddress(byte[] publicKeyHash) {
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
}

View File

@ -18,8 +18,8 @@ public class TradeBotData {
private byte[] tradePrivateKey;
public enum State {
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(15), BOB_WAITING_FOR_P2SH_B(20), BOB_WAITING_FOR_AT_REDEEM(25), BOB_DONE(30),
ALICE_WAITING_FOR_P2SH_A(80), ALICE_WAITING_FOR_AT_LOCK(85), ALICE_WATCH_P2SH_B(90), ALICE_REFUNDING_B(95), ALICE_REFUNDING_A(100), ALICE_DONE(105);
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(15), BOB_WAITING_FOR_P2SH_B(20), BOB_WAITING_FOR_AT_REDEEM(25), BOB_DONE(30), BOB_REFUNDED(35),
ALICE_WAITING_FOR_P2SH_A(80), ALICE_WAITING_FOR_AT_LOCK(85), ALICE_WATCH_P2SH_B(90), ALICE_DONE(95), ALICE_REFUNDING_B(100), ALICE_REFUNDING_A(105), ALICE_REFUNDED(110);
public final int value;
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));