@ -8,7 +8,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.logging.log4j.LogManager ;
import org.apache.logging.log4j.Logger ;
import org.bitcoinj.core.Coin ;
import org.bitcoinj.core.ECKey ;
import org.bitcoinj.core.Transaction ;
import org.bitcoinj.core.TransactionOutput ;
import org.qortal.account.PrivateKeyAccount ;
import org.qortal.account.PublicKeyAccount ;
import org.qortal.api.model.TradeBotCreateRequest ;
@ -38,7 +41,8 @@ public class TradeBot {
private static final Logger LOGGER = LogManager . getLogger ( TradeBot . class ) ;
private static final Random RANDOM = new SecureRandom ( ) ;
private static final long FEE_AMOUNT = 1000L ;
private static TradeBot instance ;
/** To help ensure only TradeBot is only active on one thread. */
@ -61,7 +65,7 @@ public class TradeBot {
byte [ ] tradeNativePublicKey = deriveTradeNativePublicKey ( tradePrivateKey ) ;
byte [ ] tradeNativePublicKeyHash = Crypto . hash160 ( tradeNativePublicKey ) ;
String tradeAddress = Crypto . toAddress ( tradeNativePublicKey ) ;
String tradeNative Address = Crypto . toAddress ( tradeNativePublicKey ) ;
byte [ ] tradeForeignPublicKey = deriveTradeForeignPublicKey ( tradePrivateKey ) ;
byte [ ] tradeForeignPublicKeyHash = Crypto . hash160 ( tradeForeignPublicKey ) ;
@ -79,7 +83,7 @@ public class TradeBot {
String description = "QORT/BTC cross-chain trade" ;
String aTType = "ACCT" ;
String tags = "ACCT QORT BTC" ;
byte [ ] creationBytes = BTCACCT . buildQortalAT ( tradeAddress , tradeForeignPublicKeyHash , hashOfSecretB , tradeBotCreateRequest . qortAmount , tradeBotCreateRequest . bitcoinAmount , tradeBotCreateRequest . tradeTimeout ) ;
byte [ ] creationBytes = BTCACCT . buildQortalAT ( tradeNative Address , tradeForeignPublicKeyHash , hashOfSecretB , tradeBotCreateRequest . qortAmount , tradeBotCreateRequest . bitcoinAmount , tradeBotCreateRequest . tradeTimeout ) ;
long amount = tradeBotCreateRequest . fundingQortAmount ;
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData ( baseTransactionData , name , description , aTType , tags , creationBytes , amount , Asset . QORT ) ;
@ -93,9 +97,10 @@ public class TradeBot {
TradeBotData tradeBotData = new TradeBotData ( tradePrivateKey , TradeBotData . State . BOB_WAITING_FOR_AT_CONFIRM ,
atAddress ,
tradeNativePublicKey , tradeNativePublicKeyHash , secretB , hashOfSecretB ,
tradeNativePublicKey , tradeNativePublicKeyHash , tradeNativeAddress ,
secretB , hashOfSecretB ,
tradeForeignPublicKey , tradeForeignPublicKeyHash ,
tradeBotCreateRequest . bitcoinAmount , null , null ) ;
tradeBotCreateRequest . bitcoinAmount , null , null , null ) ;
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
@ -107,13 +112,14 @@ public class TradeBot {
}
}
public static String startResponse ( Repository repository , CrossChainTradeData crossChainTradeData ) throws DataException {
public static boolean startResponse ( Repository repository , CrossChainTradeData crossChainTradeData , String xprv58 ) throws DataException {
byte [ ] tradePrivateKey = generateTradePrivateKey ( ) ;
byte [ ] secretA = generateSecret ( ) ;
byte [ ] hashOfSecretA = Crypto . hash160 ( secretA ) ;
byte [ ] tradeNativePublicKey = deriveTradeNativePublicKey ( tradePrivateKey ) ;
byte [ ] tradeNativePublicKeyHash = Crypto . hash160 ( tradeNativePublicKey ) ;
String tradeNativeAddress = Crypto . toAddress ( tradeNativePublicKey ) ;
byte [ ] tradeForeignPublicKey = deriveTradeForeignPublicKey ( tradePrivateKey ) ;
byte [ ] tradeForeignPublicKeyHash = Crypto . hash160 ( tradeForeignPublicKey ) ;
@ -123,15 +129,36 @@ public class TradeBot {
TradeBotData tradeBotData = new TradeBotData ( tradePrivateKey , TradeBotData . State . ALICE_WAITING_FOR_P2SH_A ,
crossChainTradeData . qortalAtAddress ,
tradeNativePublicKey , tradeNativePublicKeyHash , secretA , hashOfSecretA ,
tradeNativePublicKey , tradeNativePublicKeyHash , tradeNativeAddress ,
secretA , hashOfSecretA ,
tradeForeignPublicKey , tradeForeignPublicKeyHash ,
crossChainTradeData . expectedBitcoin , null , lockTimeA ) ;
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
crossChainTradeData . expectedBitcoin , xprv58 , null , lockTimeA ) ;
// Check we have enough funds via xprv58 to fund both P2SH to cover expectedBitcoin
String tradeForeignAddress = BTC . getInstance ( ) . pkhToAddress ( tradeForeignPublicKeyHash ) ;
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
byte [ ] redeemScriptBytes = BTCP2SH . buildScript ( tradeForeignPublicKeyHash , lockTimeA , crossChainTradeData . creatorBitcoinPKH , hashOfSecretA ) ;
return BTC . getInstance ( ) . deriveP2shAddress ( redeemScriptBytes ) ;
String p2shAddress = BTC . getInstance ( ) . deriveP2shAddress ( redeemScriptBytes ) ;
// 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?" ) ) ;
return false ;
}
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
return true ;
}
private static byte [ ] generateTradePrivateKey ( ) {
@ -175,12 +202,32 @@ public class TradeBot {
handleBobWaitingForAtConfirm ( repository , tradeBotData ) ;
break ;
case ALICE_WAITING_FOR_P2SH_A :
handleAliceWaitingForP2shA ( repository , tradeBotData ) ;
break ;
case BOB_WAITING_FOR_MESSAGE :
handleBobWaitingForMessage ( repository , tradeBotData ) ;
break ;
case ALICE_WAITING_FOR_P2SH_A :
handleAliceWaitingForP2shA ( repository , tradeBotData ) ;
case ALICE_WAITING_FOR_AT_LOCK :
handleAliceWaitingForAtLock ( repository , tradeBotData ) ;
break ;
case BOB_WAITING_FOR_P2SH_B :
handleBobWaitingForP2shB ( repository , tradeBotData ) ;
break ;
case ALICE_WATCH_P2SH_B :
handleAliceWatchingP2shB ( repository , tradeBotData ) ;
break ;
case BOB_WAITING_FOR_AT_REDEEM :
handleBobWaitingForAtRedeem ( repository , tradeBotData ) ;
break ;
case ALICE_DONE :
case BOB_DONE :
break ;
default :
@ -203,6 +250,48 @@ public class TradeBot {
repository . saveChanges ( ) ;
}
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 ( ) ) ) ;
return ;
}
CrossChainTradeData crossChainTradeData = BTCACCT . populateTradeData ( repository , atData ) ;
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 ) ) ) ;
return ;
}
// Attempt to send MESSAGE to Bob's Qortal trade address
byte [ ] messageData = BTCACCT . buildOfferMessage ( tradeBotData . getTradeForeignPublicKeyHash ( ) , tradeBotData . getHashOfSecret ( ) , tradeBotData . getLockTimeA ( ) ) ;
PrivateKeyAccount sender = new PrivateKeyAccount ( repository , tradeBotData . getTradePrivateKey ( ) ) ;
MessageTransaction messageTransaction = MessageTransaction . build ( repository , sender , Group . NO_GROUP , crossChainTradeData . qortalCreatorTradeAddress , 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" , messageTransaction . getRecipient ( ) , result . name ( ) ) ) ;
return ;
}
tradeBotData . setState ( TradeBotData . State . ALICE_WAITING_FOR_AT_LOCK ) ;
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
}
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 ( ) ) ;
@ -211,7 +300,7 @@ public class TradeBot {
return ;
}
String address = Crypto . toAddress ( tradeBotData . getTradeNativePublicKey ( ) ) ;
String address = tradeBotData . getTradeNativeAddress ( ) ;
List < MessageTransactionData > messageTransactionsData = repository . getTransactionRepository ( ) . getMessagesByRecipient ( address , null , null , null ) ;
final byte [ ] originalLastTransactionSignature = tradeBotData . getLastTransactionSignature ( ) ;
@ -231,8 +320,6 @@ public class TradeBot {
if ( messageTransactionData . isText ( ) )
continue ;
// Could enforce encryption here
// We're expecting: HASH160(secret) + Alice's Bitcoin pubkeyhash
byte [ ] messageData = messageTransactionData . getData ( ) ;
BTCACCT . OfferMessageData offerMessageData = BTCACCT . extractOfferMessageData ( messageData ) ;
@ -286,7 +373,9 @@ public class TradeBot {
}
}
private void handleAliceWaitingForP2shA ( Repository repository , TradeBotData tradeBotData ) throws DataException {
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 ( ) ) ) ;
@ -294,18 +383,149 @@ public class TradeBot {
}
CrossChainTradeData crossChainTradeData = BTCACCT . populateTradeData ( repository , atData ) ;
byte [ ] redeemScriptBytes = BTCP2SH . buildScript ( tradeBotData . getTradeForeignPublicKeyHash ( ) , tradeBotData . getLockTimeA ( ) , crossChainTradeData . creatorBitcoinPKH , tradeBotData . getHashOfSecret ( ) ) ;
// We're waiting for AT to be in TRADE mode
if ( crossChainTradeData . mode ! = CrossChainTradeData . Mode . TRADE )
return ;
// 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')" ,
tradeBotData . getAtAddress ( ) ,
crossChainTradeData . qortalRecipient ,
tradeBotData . getTradeNativeAddress ( ) ) ) ;
// 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 ( ) ;
return ;
}
// 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 ) ) ;
return ;
}
// Find our message
Long recipientMessageTimestamp = null ;
for ( MessageTransactionData messageTransactionData : messageTransactionsData )
if ( Arrays . equals ( messageTransactionData . getSenderPublicKey ( ) , tradeBotData . getTradeNativePublicKey ( ) ) ) {
recipientMessageTimestamp = messageTransactionData . getTimestamp ( ) ;
break ;
}
if ( recipientMessageTimestamp = = null ) {
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
if ( lockTimeB ! = crossChainTradeData . lockTimeB ) {
LOGGER . debug ( ( ) - > String . format ( "Trade AT lockTimeB '%d' doesn't match our lockTimeB '%d'" , crossChainTradeData . lockTimeB , lockTimeB ) ) ;
// We'll eventually refund
return ;
}
byte [ ] redeemScriptBytes = BTCP2SH . buildScript ( tradeBotData . getTradeForeignPublicKeyHash ( ) , lockTimeB , crossChainTradeData . creatorBitcoinPKH , crossChainTradeData . hashOfSecretB ) ;
String p2shAddress = BTC . getInstance ( ) . deriveP2shAddress ( redeemScriptBytes ) ;
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?" ) ) ;
return ;
}
// 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 ( ) ;
}
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 ( ) ) ) ;
return ;
}
CrossChainTradeData crossChainTradeData = BTCACCT . populateTradeData ( repository , atData ) ;
// 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
return ;
byte [ ] redeemScriptBytes = BTCP2SH . buildScript ( crossChainTradeData . recipientBitcoinPKH , crossChainTradeData . lockTimeB , crossChainTradeData . creatorBitcoinPKH , crossChainTradeData . hashOfSecretB ) ;
String p2shAddress = BTC . getInstance ( ) . deriveP2shAddress ( redeemScriptBytes ) ;
Long balance = BTC . getInstance ( ) . getBalance ( p2shAddress ) ;
if ( balance = = null | | balance < crossChainTradeData . expectedBitcoin )
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 ) ) ) ;
return ;
}
// Attempt to send MESSAGE to Bob's Qortal trade address
byte [ ] messageData = BTCACCT . buildOfferMessage ( tradeBotData . getTradeForeignPublicKeyHash ( ) , tradeBotData . getHashOfSecret ( ) , tradeBotData . getLockTimeA ( ) ) ;
// 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?" ) ) ;
return ;
}
// P2SH-b redeemed, now we wait for Alice to use secret to redeem AT
tradeBotData . setState ( TradeBotData . State . BOB_WAITING_FOR_AT_REDEEM ) ;
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
}
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 ( ) ) ) ;
return ;
}
CrossChainTradeData crossChainTradeData = BTCACCT . populateTradeData ( repository , atData ) ;
byte [ ] redeemScriptBytes = BTCP2SH . buildScript ( tradeBotData . getTradeForeignPublicKeyHash ( ) , crossChainTradeData . lockTimeB , crossChainTradeData . creatorBitcoinPKH , crossChainTradeData . hashOfSecretB ) ;
String p2shAddress = BTC . getInstance ( ) . deriveP2shAddress ( redeemScriptBytes ) ;
List < byte [ ] > p2shTransactions = BTC . getInstance ( ) . getAddressTransactions ( p2shAddress ) ;
if ( p2shTransactions = = null ) {
LOGGER . debug ( ( ) - > String . format ( "Unable to fetch transactions relating to '%s'" , p2shAddress ) ) ;
return ;
}
byte [ ] secretB = BTCP2SH . findP2shSecret ( p2shAddress , p2shTransactions ) ;
if ( secretB = = null )
// Secret not revealed at this time
return ;
// Send MESSAGE to AT using both secrets
byte [ ] secretA = tradeBotData . getSecret ( ) ;
byte [ ] messageData = BTCACCT . buildRedeemMessage ( secretA , secretB ) ;
PrivateKeyAccount sender = new PrivateKeyAccount ( repository , tradeBotData . getTradePrivateKey ( ) ) ;
MessageTransaction messageTransaction = MessageTransaction . build ( repository , sender , Group . NO_GROUP , crossChainTradeData . qortalCreatorTradeAddress , messageData , false , false ) ;
MessageTransaction messageTransaction = MessageTransaction . build ( repository , sender , Group . NO_GROUP , tradeBotData . getAtAddress ( ) , messageData , false , false ) ;
messageTransaction . computeNonce ( ) ;
messageTransaction . sign ( sender ) ;
@ -319,7 +539,50 @@ public class TradeBot {
return ;
}
tradeBotData . setState ( TradeBotData . State . ALICE_WAITING_FOR_AT_LOCK ) ;
tradeBotData . setState ( TradeBotData . State . ALICE_DONE ) ;
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
}
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 ( ) ) ) ;
return ;
}
CrossChainTradeData crossChainTradeData = BTCACCT . populateTradeData ( repository , atData ) ;
// AT should be 'finished' once Alice has redeemed QORT funds
if ( ! atData . getIsFinished ( ) )
// 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 ( ) ) ) ;
return ;
}
// Use secretA to redeem P2SH-a
byte [ ] redeemScriptBytes = BTCP2SH . buildScript ( crossChainTradeData . recipientBitcoinPKH , crossChainTradeData . lockTimeA , crossChainTradeData . creatorBitcoinPKH , crossChainTradeData . hashOfSecretA ) ;
String p2shAddress = BTC . getInstance ( ) . deriveP2shAddress ( redeemScriptBytes ) ;
Coin redeemAmount = Coin . valueOf ( crossChainTradeData . expectedBitcoin ) ;
ECKey redeemKey = ECKey . fromPrivate ( tradeBotData . getTradePrivateKey ( ) ) ;
List < TransactionOutput > fundingOutputs = BTC . getInstance ( ) . getUnspentOutputs ( p2shAddress ) ;
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?" ) ) ;
return ;
}
tradeBotData . setState ( TradeBotData . State . BOB_DONE ) ;
repository . getCrossChainRepository ( ) . save ( tradeBotData ) ;
repository . saveChanges ( ) ;
}