Merge pull request #4846 from bisq-network/release/v1.5.0

Release/v1.5.0
This commit is contained in:
sqrrm 2020-11-26 12:42:14 +01:00 committed by GitHub
commit 3cc9e63593
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
181 changed files with 9905 additions and 3032 deletions

View file

@ -28,7 +28,7 @@ configure(subprojects) {
ext { // in alphabetical order
bcVersion = '1.63'
bitcoinjVersion = 'a733034'
bitcoinjVersion = '7752cb7'
btcdCli4jVersion = '27b94333'
codecVersion = '1.13'
easybindVersion = '1.0.3'
@ -386,7 +386,7 @@ configure(project(':desktop')) {
apply plugin: 'witness'
apply from: '../gradle/witness/gradle-witness.gradle'
version = '1.4.2-SNAPSHOT'
version = '1.5.0-SNAPSHOT'
mainClassName = 'bisq.desktop.app.BisqAppMain'

View file

@ -30,14 +30,14 @@ public class Version {
// VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update
// Therefore all sub versions start again with 1
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.4.2";
public static final String VERSION = "1.5.0";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
* This must not contain each version but only those where we add new version-tagged resource files for
* historical data stores.
*/
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0");
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0");
public static int getMajorVersion(String version) {
return getSubVersion(version, 0);
@ -92,10 +92,13 @@ public class Version {
// The version no. of the current protocol. The offer holds that version.
// A taker will check the version of the offers to see if his version is compatible.
// Offers created with the old version will become invalid and have to be canceled.
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Bisq app.
// VERSION = 0.5.0 -> TRADE_PROTOCOL_VERSION = 1
// Version 1.2.2 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2;
// Version 1.5.0 -> TRADE_PROTOCOL_VERSION = 3
public static final int TRADE_PROTOCOL_VERSION = 3;
private static int p2pMessageVersion;
public static final String BSQ_TX_VERSION = "1";

View file

@ -72,7 +72,7 @@ public enum BaseCurrencyNetwork {
return "BTC_REGTEST".equals(name());
}
public long getDefaultMinFeePerByte() {
public long getDefaultMinFeePerVbyte() {
return 2;
}
}

View file

@ -44,6 +44,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@ -80,43 +81,59 @@ public class PersistenceManager<T extends PersistableEnvelope> {
///////////////////////////////////////////////////////////////////////////////////////////
public static final Map<String, PersistenceManager<?>> ALL_PERSISTENCE_MANAGERS = new HashMap<>();
public static boolean FLUSH_ALL_DATA_TO_DISK_CALLED = false;
// We don't know from which thread we are called so we map back to user thread
// We require being called only once from the global shutdown routine. As the shutdown routine has a timeout
// and error condition where we call the method as well beside the standard path and it could be that those
// alternative code paths call our method after it was called already, so it is a valid but rare case.
// We add a guard to prevent repeated calls.
public static void flushAllDataToDisk(ResultHandler completeHandler) {
log.info("Start flushAllDataToDisk at shutdown");
AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size());
// We don't know from which thread we are called so we map to user thread
UserThread.execute(() -> {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call.");
return;
}
if (openInstances.get() == 0) {
log.info("flushAllDataToDisk completed");
UserThread.execute(completeHandler::handleResult);
}
FLUSH_ALL_DATA_TO_DISK_CALLED = true;
new HashSet<>(ALL_PERSISTENCE_MANAGERS.values()).forEach(persistenceManager -> {
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
// a requestPersistence call after an important state update. Those are usually rather small data stores.
// Otherwise we only persist if requestPersistence was called since the last persist call.
if (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested) {
// We don't know from which thread we are called so we map back to user thread when calling persistNow
UserThread.execute(() -> {
log.info("Start flushAllDataToDisk at shutdown");
AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size());
if (openInstances.get() == 0) {
log.info("No PersistenceManager instances have been created yet.");
completeHandler.handleResult();
}
new HashSet<>(ALL_PERSISTENCE_MANAGERS.values()).forEach(persistenceManager -> {
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
// a requestPersistence call after an important state update. Those are usually rather small data stores.
// Otherwise we only persist if requestPersistence was called since the last persist call.
if (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested) {
// We always get our completeHandler called even if exceptions happen. In case a file write fails
// we still call our shutdown and count down routine as the completeHandler is triggered in any case.
// We get our result handler called from the write thread so we map back to user thread.
persistenceManager.persistNow(() ->
onWriteCompleted(completeHandler, openInstances, persistenceManager));
});
} else {
onWriteCompleted(completeHandler, openInstances, persistenceManager);
}
UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager)));
} else {
onWriteCompleted(completeHandler, openInstances, persistenceManager);
}
});
});
}
// We get called always from user thread here.
private static void onWriteCompleted(ResultHandler completeHandler,
AtomicInteger openInstances,
PersistenceManager<?> persistenceManager) {
persistenceManager.shutdown();
if (openInstances.decrementAndGet() == 0) {
log.info("flushAllDataToDisk completed");
UserThread.execute(completeHandler::handleResult);
completeHandler.handleResult();
}
}
@ -126,25 +143,25 @@ public class PersistenceManager<T extends PersistableEnvelope> {
public enum Source {
// For data stores we received from the network and which could be rebuilt. We store only for avoiding too much network traffic.
NETWORK(1, TimeUnit.HOURS.toSeconds(1), false),
NETWORK(1, TimeUnit.MINUTES.toMillis(5), false),
// For data stores which are created from private local data. This data could only be rebuilt from backup files.
PRIVATE(10, TimeUnit.SECONDS.toSeconds(30), true),
PRIVATE(10, 200, true),
// For data stores which are created from private local data. Loss of that data would not have any critical consequences.
PRIVATE_LOW_PRIO(4, TimeUnit.HOURS.toSeconds(2), false);
// For data stores which are created from private local data. Loss of that data would not have critical consequences.
PRIVATE_LOW_PRIO(4, TimeUnit.MINUTES.toMillis(1), false);
@Getter
private final int numMaxBackupFiles;
@Getter
private final long delayInSec;
private final long delay;
@Getter
private final boolean flushAtShutDown;
Source(int numMaxBackupFiles, long delayInSec, boolean flushAtShutDown) {
Source(int numMaxBackupFiles, long delay, boolean flushAtShutDown) {
this.numMaxBackupFiles = numMaxBackupFiles;
this.delayInSec = delayInSec;
this.delay = delay;
this.flushAtShutDown = flushAtShutDown;
}
}
@ -166,6 +183,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
@Nullable
private Timer timer;
private ExecutorService writeToDiskExecutor;
public final AtomicBoolean initCalled = new AtomicBoolean(false);
///////////////////////////////////////////////////////////////////////////////////////////
@ -190,6 +208,29 @@ public class PersistenceManager<T extends PersistableEnvelope> {
}
public void initialize(T persistable, String fileName, Source source) {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
log.warn("We have started the shut down routine already. We ignore that initialize call.");
return;
}
if (ALL_PERSISTENCE_MANAGERS.containsKey(fileName)) {
RuntimeException runtimeException = new RuntimeException("We must not create multiple " +
"PersistenceManager instances for file " + fileName + ".");
// We want to get logged from where we have been called so lets print the stack trace.
runtimeException.printStackTrace();
throw runtimeException;
}
if (initCalled.get()) {
RuntimeException runtimeException = new RuntimeException("We must not call initialize multiple times. " +
"PersistenceManager for file: " + fileName + ".");
// We want to get logged from where we have been called so lets print the stack trace.
runtimeException.printStackTrace();
throw runtimeException;
}
initCalled.set(true);
this.persistable = persistable;
this.fileName = fileName;
this.source = source;
@ -233,6 +274,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
* @param orElse Called if no file exists or reading of file failed.
*/
public void readPersisted(String fileName, Consumer<T> resultHandler, Runnable orElse) {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
log.warn("We have started the shut down routine already. We ignore that readPersisted call.");
return;
}
new Thread(() -> {
T persisted = getPersisted(fileName);
if (persisted != null) {
@ -252,6 +298,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
@Nullable
public T getPersisted(String fileName) {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
log.warn("We have started the shut down routine already. We ignore that getPersisted call.");
return null;
}
File storageFile = new File(dir, fileName);
if (!storageFile.exists()) {
return null;
@ -288,6 +339,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
///////////////////////////////////////////////////////////////////////////////////////////
public void requestPersistence() {
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
log.warn("We have started the shut down routine already. We ignore that requestPersistence call.");
return;
}
persistenceRequested = true;
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
@ -296,7 +352,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
timer = UserThread.runAfter(() -> {
persistNow(null);
UserThread.execute(() -> timer = null);
}, source.delayInSec, TimeUnit.SECONDS);
}, source.delay, TimeUnit.MILLISECONDS);
}
}
@ -398,7 +454,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
",\n dir=" + dir +
",\n storageFile=" + storageFile +
",\n persistable=" + persistable +
",\n priority=" + source +
",\n source=" + source +
",\n usedTempFilePath=" + usedTempFilePath +
",\n persistenceRequested=" + persistenceRequested +
"\n}";

View file

@ -133,7 +133,7 @@ public class PrivateNotificationManager {
}
public void removePrivateNotification() {
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
p2PService.removeMailboxMsg(decryptedMessageWithPubKey);
}
private boolean isKeyValid(String privKeyString) {

View file

@ -68,6 +68,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
protected AppModule module;
protected Config config;
private boolean isShutdownInProgress;
private boolean hasDowngraded;
public BisqExecutable(String fullName, String scriptName, String appName, String version) {
this.fullName = fullName;
@ -133,9 +134,17 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
CommonSetup.setupUncaughtExceptionHandler(this);
setupGuice();
setupAvoidStandbyMode();
readAllPersisted(this::startApplication);
}
hasDowngraded = BisqSetup.hasDowngraded();
if (hasDowngraded) {
// If user tried to downgrade we do not read the persisted data to avoid data corruption
// We call startApplication to enable UI to show popup. We prevent in BisqSetup to go further
// in the process and require a shut down.
startApplication();
} else {
readAllPersisted(this::startApplication);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
@ -236,11 +245,16 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
injector.getInstance(P2PService.class).shutDown(() -> {
log.info("P2PService shutdown completed");
module.close(injector);
PersistenceManager.flushAllDataToDisk(() -> {
log.info("Graceful shutdown completed. Exiting now.");
resultHandler.handleResult();
System.exit(EXIT_SUCCESS);
});
if (!hasDowngraded) {
// If user tried to downgrade we do not write the persistable data to avoid data corruption
PersistenceManager.flushAllDataToDisk(() -> {
log.info("Graceful shutdown completed. Exiting now.");
resultHandler.handleResult();
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
});
} else {
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
}
});
});
walletsSetup.shutDown();
@ -250,20 +264,31 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
// Wait max 20 sec.
UserThread.runAfter(() -> {
log.warn("Timeout triggered resultHandler");
PersistenceManager.flushAllDataToDisk(() -> {
log.info("Graceful shutdown resulted in a timeout. Exiting now.");
resultHandler.handleResult();
System.exit(EXIT_SUCCESS);
});
if (!hasDowngraded) {
// If user tried to downgrade we do not write the persistable data to avoid data corruption
PersistenceManager.flushAllDataToDisk(() -> {
log.info("Graceful shutdown resulted in a timeout. Exiting now.");
resultHandler.handleResult();
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
});
} else {
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
}
}, 20);
} catch (Throwable t) {
log.error("App shutdown failed with exception {}", t.toString());
t.printStackTrace();
PersistenceManager.flushAllDataToDisk(() -> {
log.info("Graceful shutdown resulted in an error. Exiting now.");
resultHandler.handleResult();
System.exit(EXIT_FAILURE);
});
if (!hasDowngraded) {
// If user tried to downgrade we do not write the persistable data to avoid data corruption
PersistenceManager.flushAllDataToDisk(() -> {
log.info("Graceful shutdown resulted in an error. Exiting now.");
resultHandler.handleResult();
UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1);
});
} else {
UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1);
}
}
}

View file

@ -20,6 +20,7 @@ package bisq.core.app;
import bisq.core.trade.TradeManager;
import bisq.common.UserThread;
import bisq.common.app.Version;
import bisq.common.file.CorruptedStorageFileHandler;
import bisq.common.setup.GracefulShutDownHandler;
@ -94,6 +95,8 @@ public class BisqHeadlessApp implements HeadlessApp {
bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
bisqSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported",
lastVersion, Version.VERSION));
corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files));
tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("onTakeOfferRequestErrorMessageHandler"));

View file

@ -48,6 +48,7 @@ import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.app.Log;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.util.InvalidVersionException;
import bisq.common.util.Utilities;
@ -71,11 +72,15 @@ import javafx.collections.SetChangeListener;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
@ -92,6 +97,7 @@ import javax.annotation.Nullable;
@Slf4j
@Singleton
public class BisqSetup {
private static final String VERSION_FILE_NAME = "version";
public interface BisqSetupListener {
default void onInitP2pNetwork() {
@ -172,6 +178,9 @@ public class BisqSetup {
@Setter
@Nullable
private Runnable qubesOSInfoHandler;
@Setter
@Nullable
private Consumer<String> downGradePreventionHandler;
@Getter
final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false);
@ -255,6 +264,12 @@ public class BisqSetup {
}
public void start() {
// If user tried to downgrade we require a shutdown
if (hasDowngraded(downGradePreventionHandler)) {
return;
}
persistBisqVersion();
maybeReSyncSPVChain();
maybeShowTac(this::step2);
}
@ -387,7 +402,7 @@ public class BisqSetup {
requestWalletPasswordHandler.accept(aesKey -> {
walletsManager.setAesKey(aesKey);
walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(),
aesKey);
aesKey);
if (preferences.isResyncSpvRequested()) {
if (showFirstPopupIfResyncSPVRequestedHandler != null)
showFirstPopupIfResyncSPVRequestedHandler.run();
@ -487,6 +502,68 @@ public class BisqSetup {
});
}
@Nullable
public static String getLastBisqVersion() {
File versionFile = getVersionFile();
if (!versionFile.exists()) {
return null;
}
try (Scanner scanner = new Scanner(versionFile)) {
// We only expect 1 line
if (scanner.hasNextLine()) {
return scanner.nextLine();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
private static File getVersionFile() {
return new File(Config.appDataDir(), VERSION_FILE_NAME);
}
public static boolean hasDowngraded() {
return hasDowngraded(getLastBisqVersion());
}
public static boolean hasDowngraded(String lastVersion) {
return lastVersion != null && Version.isNewVersion(lastVersion, Version.VERSION);
}
public static boolean hasDowngraded(@Nullable Consumer<String> downGradePreventionHandler) {
String lastVersion = getLastBisqVersion();
boolean hasDowngraded = hasDowngraded(lastVersion);
if (hasDowngraded) {
log.error("Downgrade from version {} to version {} is not supported", lastVersion, Version.VERSION);
if (downGradePreventionHandler != null) {
downGradePreventionHandler.accept(lastVersion);
}
}
return hasDowngraded;
}
public static void persistBisqVersion() {
File versionFile = getVersionFile();
if (!versionFile.exists()) {
try {
if (!versionFile.createNewFile()) {
log.error("Version file could not be created");
}
} catch (IOException e) {
e.printStackTrace();
log.error("Version file could not be created. {}", e.toString());
}
}
try (FileWriter fileWriter = new FileWriter(versionFile, false)) {
fileWriter.write(Version.VERSION);
} catch (IOException e) {
e.printStackTrace();
log.error("Writing Version failed. {}", e.toString());
}
}
private void checkForCorrectOSArchitecture() {
if (!Utilities.isCorrectOSArchitecture() && wrongOSArchitectureHandler != null) {
String osArchitecture = Utilities.getOSArchitecture();

View file

@ -22,8 +22,8 @@ import bisq.core.btc.exceptions.RejectedTxException;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.WalletsManager;
import bisq.core.locale.Res;
import bisq.core.provider.fee.FeeService;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.fee.FeeService;
import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
@ -107,7 +107,7 @@ public class WalletAppSetup {
Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) {
log.info("Initialize WalletAppSetup with BitcoinJ version {} and hash of BitcoinJ commit {}",
VersionMessage.BITCOINJ_VERSION, "a733034");
VersionMessage.BITCOINJ_VERSION, "7752cb7");
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(),
@ -247,6 +247,7 @@ public class WalletAppSetup {
String finalDetails = details;
UserThread.runAfter(() -> {
trade.setErrorMessage(newValue.getMessage());
tradeManager.requestPersistence();
if (rejectedTxErrorMessageHandler != null) {
rejectedTxErrorMessageHandler.accept(Res.get("popup.warning.trade.txRejected",
finalDetails, trade.getShortId(), txId));

View file

@ -95,7 +95,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
PersistenceManager.flushAllDataToDisk(() -> {
resultHandler.handleResult();
log.info("Graceful shutdown completed. Exiting now.");
System.exit(BisqExecutable.EXIT_SUCCESS);
UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_SUCCESS), 1);
});
});
injector.getInstance(WalletsSetup.class).shutDown();
@ -107,7 +107,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
PersistenceManager.flushAllDataToDisk(() -> {
resultHandler.handleResult();
log.info("Graceful shutdown caused a timeout. Exiting now.");
System.exit(BisqExecutable.EXIT_SUCCESS);
UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_SUCCESS), 1);
});
}, 5);
} else {
@ -122,7 +122,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
PersistenceManager.flushAllDataToDisk(() -> {
resultHandler.handleResult();
log.info("Graceful shutdown resulted in an error. Exiting now.");
System.exit(BisqExecutable.EXIT_FAILURE);
UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_FAILURE), 1);
});
}

View file

@ -41,9 +41,23 @@ import static com.google.common.base.Preconditions.checkArgument;
*/
@Slf4j
public class TxFeeEstimationService {
public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 260;
private static int DEPOSIT_TX_SIZE = 320;
private static int PAYOUT_TX_SIZE = 380;
// Size/vsize of typical trade txs
// Real txs size/vsize may vary in 1 or 2 bytes from the estimated values.
// Values calculated with https://gist.github.com/oscarguindzberg/3d1349cb65d9fd9af9de0feaa3fd27ac
// legacy fee tx with 1 input, maker/taker fee paid in btc size/vsize = 258
// legacy deposit tx without change size/vsize = 381
// legacy deposit tx with change size/vsize = 414
// legacy payout tx size/vsize = 337
// legacy delayed payout tx size/vsize = 302
// segwit fee tx with 1 input, maker/taker fee paid in btc vsize = 173
// segwit deposit tx without change vsize = 232
// segwit deposit tx with change vsize = 263
// segwit payout tx vsize = 169
// segwit delayed payout tx vsize = 139
public static int TYPICAL_TX_WITH_1_INPUT_VSIZE = 175;
private static int DEPOSIT_TX_VSIZE = 233;
private static int BSQ_INPUT_INCREASE = 150;
private static int MAX_ITERATIONS = 10;
@ -61,8 +75,8 @@ public class TxFeeEstimationService {
this.preferences = preferences;
}
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSizeForTaker(Coin fundsNeededForTrade, Coin tradeFee) {
return getEstimatedFeeAndTxSize(true,
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsizeForTaker(Coin fundsNeededForTrade, Coin tradeFee) {
return getEstimatedFeeAndTxVsize(true,
fundsNeededForTrade,
tradeFee,
feeService,
@ -70,9 +84,9 @@ public class TxFeeEstimationService {
preferences);
}
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSizeForMaker(Coin reservedFundsForOffer,
Coin tradeFee) {
return getEstimatedFeeAndTxSize(false,
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsizeForMaker(Coin reservedFundsForOffer,
Coin tradeFee) {
return getEstimatedFeeAndTxVsize(false,
reservedFundsForOffer,
tradeFee,
feeService,
@ -80,120 +94,120 @@ public class TxFeeEstimationService {
preferences);
}
private Tuple2<Coin, Integer> getEstimatedFeeAndTxSize(boolean isTaker,
Coin amount,
Coin tradeFee,
FeeService feeService,
BtcWalletService btcWalletService,
Preferences preferences) {
Coin txFeePerByte = feeService.getTxFeePerByte();
// We start with min taker fee size of 260
int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE;
private Tuple2<Coin, Integer> getEstimatedFeeAndTxVsize(boolean isTaker,
Coin amount,
Coin tradeFee,
FeeService feeService,
BtcWalletService btcWalletService,
Preferences preferences) {
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
// We start with min taker fee vsize of 175
int estimatedTxVsize = TYPICAL_TX_WITH_1_INPUT_VSIZE;
try {
estimatedTxSize = getEstimatedTxSize(List.of(tradeFee, amount), estimatedTxSize, txFeePerByte, btcWalletService);
estimatedTxVsize = getEstimatedTxVsize(List.of(tradeFee, amount), estimatedTxVsize, txFeePerVbyte, btcWalletService);
} catch (InsufficientMoneyException e) {
if (isTaker) {
// if we cannot do the estimation we use the payout tx size
estimatedTxSize = PAYOUT_TX_SIZE;
// If we cannot do the estimation, we use the vsize o the largest of our txs which is the deposit tx.
estimatedTxVsize = DEPOSIT_TX_VSIZE;
}
log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " +
"if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize);
"if the user pays from an external wallet. In that case we use an estimated tx vsize of {} vbytes.", estimatedTxVsize);
}
if (!preferences.isPayFeeInBtc()) {
// If we pay the fee in BSQ we have one input more which adds about 150 bytes
// TODO: Clarify if there is always just one additional input or if there can be more.
estimatedTxSize += BSQ_INPUT_INCREASE;
estimatedTxVsize += BSQ_INPUT_INCREASE;
}
Coin txFee;
int size;
int vsize;
if (isTaker) {
int averageSize = (estimatedTxSize + DEPOSIT_TX_SIZE) / 2; // deposit tx has about 320 bytes
// We use at least the size of the payout tx to not underpay at payout.
size = Math.max(PAYOUT_TX_SIZE, averageSize);
txFee = txFeePerByte.multiply(size);
log.info("Fee estimation resulted in a tx size of {} bytes.\n" +
"We use an average between the taker fee tx and the deposit tx (320 bytes) which results in {} bytes.\n" +
"The payout tx has 380 bytes, we use that as our min value. Size for fee calculation is {} bytes.\n" +
"The tx fee of {} Sat", estimatedTxSize, averageSize, size, txFee.value);
int averageVsize = (estimatedTxVsize + DEPOSIT_TX_VSIZE) / 2; // deposit tx has about 233 vbytes
// We use at least the vsize of the deposit tx to not underpay it.
vsize = Math.max(DEPOSIT_TX_VSIZE, averageVsize);
txFee = txFeePerVbyte.multiply(vsize);
log.info("Fee estimation resulted in a tx vsize of {} vbytes.\n" +
"We use an average between the taker fee tx and the deposit tx (233 vbytes) which results in {} vbytes.\n" +
"The deposit tx has 233 vbytes, we use that as our min value. Vsize for fee calculation is {} vbytes.\n" +
"The tx fee of {} Sat", estimatedTxVsize, averageVsize, vsize, txFee.value);
} else {
size = estimatedTxSize;
txFee = txFeePerByte.multiply(size);
log.info("Fee estimation resulted in a tx size of {} bytes and a tx fee of {} Sat.", size, txFee.value);
vsize = estimatedTxVsize;
txFee = txFeePerVbyte.multiply(vsize);
log.info("Fee estimation resulted in a tx vsize of {} vbytes and a tx fee of {} Sat.", vsize, txFee.value);
}
return new Tuple2<>(txFee, size);
return new Tuple2<>(txFee, vsize);
}
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSize(Coin amount,
FeeService feeService,
BtcWalletService btcWalletService) {
Coin txFeePerByte = feeService.getTxFeePerByte();
// We start with min taker fee size of 260
int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE;
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsize(Coin amount,
FeeService feeService,
BtcWalletService btcWalletService) {
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
// We start with min taker fee vsize of 175
int estimatedTxVsize = TYPICAL_TX_WITH_1_INPUT_VSIZE;
try {
estimatedTxSize = getEstimatedTxSize(List.of(amount), estimatedTxSize, txFeePerByte, btcWalletService);
estimatedTxVsize = getEstimatedTxVsize(List.of(amount), estimatedTxVsize, txFeePerVbyte, btcWalletService);
} catch (InsufficientMoneyException e) {
log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " +
"if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize);
"if the user pays from an external wallet. In that case we use an estimated tx vsize of {} vbytes.", estimatedTxVsize);
}
Coin txFee = txFeePerByte.multiply(estimatedTxSize);
log.info("Fee estimation resulted in a tx size of {} bytes and a tx fee of {} Sat.", estimatedTxSize, txFee.value);
Coin txFee = txFeePerVbyte.multiply(estimatedTxVsize);
log.info("Fee estimation resulted in a tx vsize of {} vbytes and a tx fee of {} Sat.", estimatedTxVsize, txFee.value);
return new Tuple2<>(txFee, estimatedTxSize);
return new Tuple2<>(txFee, estimatedTxVsize);
}
// We start with the initialEstimatedTxSize for a tx with 1 input (260) bytes and get from BitcoinJ a tx back which
// We start with the initialEstimatedTxVsize for a tx with 1 input (175) vbytes and get from BitcoinJ a tx back which
// contains the required inputs to fund that tx (outputs + miner fee). The miner fee in that case is based on
// the assumption that we only need 1 input. Once we receive back the real tx size from the tx BitcoinJ has created
// with the required inputs we compare if the size is not more then 20% different to our assumed tx size. If we are inside
// that tolerance we use that tx size for our fee estimation, if not (if there has been more then 1 inputs) we
// apply the new fee based on the reported tx size and request again from BitcoinJ to fill that tx with the inputs
// the assumption that we only need 1 input. Once we receive back the real tx vsize from the tx BitcoinJ has created
// with the required inputs we compare if the vsize is not more then 20% different to our assumed tx vsize. If we are inside
// that tolerance we use that tx vsize for our fee estimation, if not (if there has been more then 1 inputs) we
// apply the new fee based on the reported tx vsize and request again from BitcoinJ to fill that tx with the inputs
// to be sufficiently funded. The algorithm how BitcoinJ selects utxos is complex and contains several aspects
// (minimize fee, don't create too many tiny utxos,...). We treat that algorithm as an unknown and it is not
// guaranteed that there are more inputs required if we increase the fee (it could be that there is a better
// selection of inputs chosen if we have increased the fee and therefore less inputs and smaller tx size). As the increased fee might
// selection of inputs chosen if we have increased the fee and therefore less inputs and smaller tx vsize). As the increased fee might
// change the number of inputs we need to repeat that process until we are inside of a certain tolerance. To avoid
// potential endless loops we add a counter (we use 10, usually it takes just very few iterations).
// Worst case would be that the last size we got reported is > 20% off to
// the real tx size but as fee estimation is anyway a educated guess in the best case we don't worry too much.
// Worst case would be that the last vsize we got reported is > 20% off to
// the real tx vsize but as fee estimation is anyway a educated guess in the best case we don't worry too much.
// If we have underpaid the tx might take longer to get confirmed.
@VisibleForTesting
static int getEstimatedTxSize(List<Coin> outputValues,
int initialEstimatedTxSize,
Coin txFeePerByte,
BtcWalletService btcWalletService)
static int getEstimatedTxVsize(List<Coin> outputValues,
int initialEstimatedTxVsize,
Coin txFeePerVbyte,
BtcWalletService btcWalletService)
throws InsufficientMoneyException {
boolean isInTolerance;
int estimatedTxSize = initialEstimatedTxSize;
int realTxSize;
int estimatedTxVsize = initialEstimatedTxVsize;
int realTxVsize;
int counter = 0;
do {
Coin txFee = txFeePerByte.multiply(estimatedTxSize);
realTxSize = btcWalletService.getEstimatedFeeTxSize(outputValues, txFee);
isInTolerance = isInTolerance(estimatedTxSize, realTxSize, 0.2);
Coin txFee = txFeePerVbyte.multiply(estimatedTxVsize);
realTxVsize = btcWalletService.getEstimatedFeeTxVsize(outputValues, txFee);
isInTolerance = isInTolerance(estimatedTxVsize, realTxVsize, 0.2);
if (!isInTolerance) {
estimatedTxSize = realTxSize;
estimatedTxVsize = realTxVsize;
}
counter++;
}
while (!isInTolerance && counter < MAX_ITERATIONS);
if (!isInTolerance) {
log.warn("We could not find a tx which satisfies our tolerance requirement of 20%. " +
"realTxSize={}, estimatedTxSize={}",
realTxSize, estimatedTxSize);
"realTxVsize={}, estimatedTxVsize={}",
realTxVsize, estimatedTxVsize);
}
return estimatedTxSize;
return estimatedTxVsize;
}
@VisibleForTesting
static boolean isInTolerance(int estimatedSize, int txSize, double tolerance) {
checkArgument(estimatedSize > 0, "estimatedSize must be positive");
checkArgument(txSize > 0, "txSize must be positive");
static boolean isInTolerance(int estimatedVsize, int txVsize, double tolerance) {
checkArgument(estimatedVsize > 0, "estimatedVsize must be positive");
checkArgument(txVsize > 0, "txVsize must be positive");
checkArgument(tolerance > 0, "tolerance must be positive");
double deviation = Math.abs(1 - ((double) estimatedSize / (double) txSize));
double deviation = Math.abs(1 - ((double) estimatedVsize / (double) txVsize));
return deviation <= tolerance;
}
}

View file

@ -97,10 +97,6 @@ public final class AddressEntry implements PersistablePayload {
Context context,
@Nullable String offerId,
boolean segwit) {
if (segwit && (!Context.AVAILABLE.equals(context) || offerId != null)) {
throw new IllegalArgumentException("Segwit addresses are only allowed for " +
"AVAILABLE entries without an offerId");
}
this.keyPair = keyPair;
this.context = context;
this.offerId = offerId;

View file

@ -28,13 +28,14 @@ import bisq.core.provider.fee.FeeService;
import bisq.core.user.Preferences;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.SegwitAddress;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput;
@ -44,6 +45,7 @@ import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptPattern;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
@ -218,8 +220,8 @@ public class BtcWalletService extends WalletService {
// estimated size of input sig
int sigSizePerInput = 106;
// typical size for a tx with 3 inputs
int txSizeWithUnsignedInputs = 300;
Coin txFeePerByte = feeService.getTxFeePerByte();
int txVsizeWithUnsignedInputs = 300;
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
Address changeAddress = getFreshAddressEntry().getAddress();
checkNotNull(changeAddress, "changeAddress must not be null");
@ -228,7 +230,9 @@ public class BtcWalletService extends WalletService {
preferences.getIgnoreDustThreshold());
List<TransactionInput> preparedBsqTxInputs = preparedTx.getInputs();
List<TransactionOutput> preparedBsqTxOutputs = preparedTx.getOutputs();
int numInputs = preparedBsqTxInputs.size();
Tuple2<Integer, Integer> numInputs = getNumInputs(preparedTx);
int numLegacyInputs = numInputs.first;
int numSegwitInputs = numInputs.second;
Transaction resultTx = null;
boolean isFeeOutsideTolerance;
do {
@ -249,7 +253,10 @@ public class BtcWalletService extends WalletService {
// signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet)
sendRequest.signInputs = false;
sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs);
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
@ -262,9 +269,14 @@ public class BtcWalletService extends WalletService {
// add OP_RETURN output
resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram()));
numInputs = resultTx.getInputs().size();
txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length;
long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value;
numInputs = getNumInputs(resultTx);
numLegacyInputs = numInputs.first;
numSegwitInputs = numInputs.second;
txVsizeWithUnsignedInputs = resultTx.getVsize();
long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
// calculated fee must be inside of a tolerance range with tx fee
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
}
@ -328,8 +340,8 @@ public class BtcWalletService extends WalletService {
// estimated size of input sig
int sigSizePerInput = 106;
// typical size for a tx with 3 inputs
int txSizeWithUnsignedInputs = 300;
Coin txFeePerByte = feeService.getTxFeePerByte();
int txVsizeWithUnsignedInputs = 300;
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
Address changeAddress = getFreshAddressEntry().getAddress();
checkNotNull(changeAddress, "changeAddress must not be null");
@ -338,7 +350,9 @@ public class BtcWalletService extends WalletService {
preferences.getIgnoreDustThreshold());
List<TransactionInput> preparedBsqTxInputs = preparedTx.getInputs();
List<TransactionOutput> preparedBsqTxOutputs = preparedTx.getOutputs();
int numInputs = preparedBsqTxInputs.size();
Tuple2<Integer, Integer> numInputs = getNumInputs(preparedTx);
int numLegacyInputs = numInputs.first;
int numSegwitInputs = numInputs.second;
Transaction resultTx = null;
boolean isFeeOutsideTolerance;
do {
@ -359,7 +373,9 @@ public class BtcWalletService extends WalletService {
// signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet)
sendRequest.signInputs = false;
sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs);
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
@ -372,9 +388,13 @@ public class BtcWalletService extends WalletService {
// add OP_RETURN output
resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram()));
numInputs = resultTx.getInputs().size();
txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length;
final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value;
numInputs = getNumInputs(resultTx);
numLegacyInputs = numInputs.first;
numSegwitInputs = numInputs.second;
txVsizeWithUnsignedInputs = resultTx.getVsize();
final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
// calculated fee must be inside of a tolerance range with tx fee
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
}
@ -466,9 +486,9 @@ public class BtcWalletService extends WalletService {
// estimated size of input sig
int sigSizePerInput = 106;
// typical size for a tx with 2 inputs
int txSizeWithUnsignedInputs = 203;
int txVsizeWithUnsignedInputs = 203;
// If useCustomTxFee we allow overriding the estimated fee from preferences
Coin txFeePerByte = useCustomTxFee ? getTxFeeForWithdrawalPerByte() : feeService.getTxFeePerByte();
Coin txFeePerVbyte = useCustomTxFee ? getTxFeeForWithdrawalPerVbyte() : feeService.getTxFeePerVbyte();
// In case there are no change outputs we force a change by adding min dust to the BTC input
Coin forcedChangeValue = Coin.ZERO;
@ -479,7 +499,10 @@ public class BtcWalletService extends WalletService {
preferences.getIgnoreDustThreshold());
List<TransactionInput> preparedBsqTxInputs = preparedBsqTx.getInputs();
List<TransactionOutput> preparedBsqTxOutputs = preparedBsqTx.getOutputs();
int numInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input
// We don't know at this point what type the btc input would be (segwit/legacy).
// We use legacy to be on the safe side.
int numLegacyInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input
int numSegwitInputs = 0;
Transaction resultTx = null;
boolean isFeeOutsideTolerance;
boolean opReturnIsOnlyOutput;
@ -508,7 +531,9 @@ public class BtcWalletService extends WalletService {
// signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet)
sendRequest.signInputs = false;
sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs);
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
@ -528,15 +553,19 @@ public class BtcWalletService extends WalletService {
if (opReturnData != null)
resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram()));
numInputs = resultTx.getInputs().size();
txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length;
final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value;
Tuple2<Integer, Integer> numInputs = getNumInputs(resultTx);
numLegacyInputs = numInputs.first;
numSegwitInputs = numInputs.second;
txVsizeWithUnsignedInputs = resultTx.getVsize();
final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
// calculated fee must be inside of a tolerance range with tx fee
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
}
while (opReturnIsOnlyOutput ||
isFeeOutsideTolerance ||
resultTx.getFee().value < txFeePerByte.multiply(resultTx.bitcoinSerialize().length).value);
resultTx.getFee().value < txFeePerVbyte.multiply(resultTx.getVsize()).value);
// Sign all BTC inputs
signAllBtcInputs(preparedBsqTxInputs.size(), resultTx);
@ -548,6 +577,25 @@ public class BtcWalletService extends WalletService {
return resultTx;
}
private Tuple2<Integer, Integer> getNumInputs(Transaction tx) {
int numLegacyInputs = 0;
int numSegwitInputs = 0;
for (TransactionInput input : tx.getInputs()) {
TransactionOutput connectedOutput = input.getConnectedOutput();
if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) ||
ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) {
// If connectedOutput is null, we don't know here the input type. To avoid underpaying fees,
// we treat it as a legacy input which will result in a higher fee estimation.
numLegacyInputs++;
} else if (ScriptPattern.isP2WPKH(connectedOutput.getScriptPubKey())) {
numSegwitInputs++;
} else {
throw new IllegalArgumentException("Inputs should spend a P2PKH, P2PK or P2WPKH ouput");
}
}
return new Tuple2(numLegacyInputs, numSegwitInputs);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Commit tx
@ -579,18 +627,17 @@ public class BtcWalletService extends WalletService {
if (addressEntry.isPresent()) {
return addressEntry.get();
} else {
// We still use non-segwit addresses for the trade protocol.
// We try to use available and not yet used entries
Optional<AddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream()
.filter(e -> AddressEntry.Context.AVAILABLE == e.getContext())
.filter(e -> isAddressUnused(e.getAddress()))
.filter(e -> Script.ScriptType.P2PKH.equals(e.getAddress().getOutputScriptType()))
.filter(e -> Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType()))
.findAny();
if (emptyAvailableAddressEntry.isPresent()) {
return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
} else {
DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH));
AddressEntry entry = new AddressEntry(key, context, offerId, false);
DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH));
AddressEntry entry = new AddressEntry(key, context, offerId, true);
addressEntryList.addAddressEntry(entry);
return entry;
}
@ -810,7 +857,7 @@ public class BtcWalletService extends WalletService {
);
log.info("newTransaction no. of inputs " + newTransaction.getInputs().size());
log.info("newTransaction size in kB " + newTransaction.bitcoinSerialize().length / 1024);
log.info("newTransaction vsize in vkB " + newTransaction.getVsize() / 1024);
if (!newTransaction.getInputs().isEmpty()) {
Coin amount = Coin.valueOf(newTransaction.getInputs().stream()
@ -821,13 +868,13 @@ public class BtcWalletService extends WalletService {
try {
Coin fee;
int counter = 0;
int txSize = 0;
int txVsize = 0;
Transaction tx;
SendRequest sendRequest;
Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte();
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerByte.multiply(txSize);
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
newTransaction.clearOutputs();
newTransaction.addOutput(amount.subtract(fee), toAddress);
@ -840,7 +887,7 @@ public class BtcWalletService extends WalletService {
sendRequest.changeAddress = toAddress;
wallet.completeTx(sendRequest);
tx = sendRequest.tx;
txSize = tx.bitcoinSerialize().length;
txVsize = tx.getVsize();
printTx("FeeEstimationTransaction", tx);
sendRequest.tx.getOutputs().forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString()));
}
@ -939,16 +986,16 @@ public class BtcWalletService extends WalletService {
try {
Coin fee;
int counter = 0;
int txSize = 0;
int txVsize = 0;
Transaction tx;
Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte();
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerByte.multiply(txSize);
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
SendRequest sendRequest = getSendRequest(fromAddress, toAddress, amount, fee, aesKey, context);
wallet.completeTx(sendRequest);
tx = sendRequest.tx;
txSize = tx.bitcoinSerialize().length;
txVsize = tx.getVsize();
printTx("FeeEstimationTransaction", tx);
}
while (feeEstimationNotSatisfied(counter, tx));
@ -986,18 +1033,20 @@ public class BtcWalletService extends WalletService {
try {
Coin fee;
int counter = 0;
int txSize = 0;
int txVsize = 0;
Transaction tx;
Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte();
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerByte.multiply(txSize);
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
// We use a dummy address for the output
final String dummyReceiver = LegacyAddress.fromKey(params, new ECKey()).toBase58();
// We don't know here whether the output is segwit or not but we don't care too much because the size of
// a segwit ouput is just 3 byte smaller than the size of a legacy ouput.
final String dummyReceiver = SegwitAddress.fromKey(params, new ECKey()).toString();
SendRequest sendRequest = getSendRequestForMultipleAddresses(fromAddresses, dummyReceiver, amount, fee, null, aesKey);
wallet.completeTx(sendRequest);
tx = sendRequest.tx;
txSize = tx.bitcoinSerialize().length;
txVsize = tx.getVsize();
printTx("FeeEstimationTransactionForMultipleAddresses", tx);
}
while (feeEstimationNotSatisfied(counter, tx));
@ -1013,16 +1062,18 @@ public class BtcWalletService extends WalletService {
}
private boolean feeEstimationNotSatisfied(int counter, Transaction tx) {
long targetFee = getTxFeeForWithdrawalPerByte().multiply(tx.bitcoinSerialize().length).value;
long targetFee = getTxFeeForWithdrawalPerVbyte().multiply(tx.getVsize()).value;
return counter < 10 &&
(tx.getFee().value < targetFee ||
tx.getFee().value - targetFee > 1000);
}
public int getEstimatedFeeTxSize(List<Coin> outputValues, Coin txFee)
public int getEstimatedFeeTxVsize(List<Coin> outputValues, Coin txFee)
throws InsufficientMoneyException, AddressFormatException {
Transaction transaction = new Transaction(params);
Address dummyAddress = LegacyAddress.fromKey(params, new ECKey());
// In reality txs have a mix of segwit/legacy ouputs, but we don't care too much because the size of
// a segwit ouput is just 3 byte smaller than the size of a legacy ouput.
Address dummyAddress = SegwitAddress.fromKey(params, new ECKey());
outputValues.forEach(outputValue -> transaction.addOutput(outputValue, dummyAddress));
SendRequest sendRequest = SendRequest.forTx(transaction);
@ -1035,7 +1086,7 @@ public class BtcWalletService extends WalletService {
sendRequest.ensureMinRequiredFee = false;
sendRequest.changeAddress = dummyAddress;
wallet.completeTx(sendRequest);
return transaction.bitcoinSerialize().length;
return transaction.getVsize();
}

View file

@ -36,8 +36,8 @@ import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.SegwitAddress;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.SignatureDecodeException;
import org.bitcoinj.core.Transaction;
@ -76,6 +76,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
public class TradeWalletService {
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
private final WalletsSetup walletsSetup;
private final Preferences preferences;
@ -336,7 +337,7 @@ public class TradeWalletService {
Transaction dummyTX = new Transaction(params);
// The output is just used to get the right inputs and change outputs, so we use an anonymous ECKey, as it will never be used for anything.
// We don't care about fee calculation differences between the real tx and that dummy tx as we use a static tx fee.
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, LegacyAddress.fromKey(params, new ECKey()));
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, SegwitAddress.fromKey(params, new ECKey()));
dummyTX.addOutput(dummyOutput);
// Find the needed inputs to pay the output, optionally add 1 change output.
@ -455,7 +456,7 @@ public class TradeWalletService {
// First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx.
// Similar to the way we did in the createTakerDepositTxInputs method.
Transaction dummyTx = new Transaction(params);
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, LegacyAddress.fromKey(params, new ECKey()));
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, SegwitAddress.fromKey(params, new ECKey()));
dummyTx.addOutput(dummyOutput);
addAvailableInputsAndChangeOutputs(dummyTx, makerAddress, makerChangeAddress);
// Normally we have only 1 input but we support multiple inputs if the user has paid in with several transactions.
@ -502,12 +503,12 @@ public class TradeWalletService {
// Add MultiSig output
Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey);
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
// Tx fee for deposit tx will be paid by buyer.
TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount,
p2SHMultiSigOutputScript.getProgram());
preparedDepositTx.addOutput(p2SHMultiSigOutput);
TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount,
hashedMultiSigOutputScript.getProgram());
preparedDepositTx.addOutput(hashedMultiSigOutput);
// We add the hash ot OP_RETURN with a 0 amount output
TransactionOutput contractHashOutput = new TransactionOutput(params, preparedDepositTx, Coin.ZERO,
@ -569,8 +570,9 @@ public class TradeWalletService {
* @param buyerPubKey the public key of the buyer
* @param sellerPubKey the public key of the seller
* @throws SigningException if (one of) the taker input(s) was of an unrecognized type for signing
* @throws TransactionVerificationException if a maker input wasn't signed, their MultiSig script or contract hash
* doesn't match the taker's, or there was an unexpected problem with the final deposit tx or its signatures
* @throws TransactionVerificationException if a non-P2WH maker-as-buyer input wasn't signed, the maker's MultiSig
* script or contract hash doesn't match the taker's, or there was an unexpected problem with the final deposit tx
* or its signatures
* @throws WalletException if the taker's wallet is null or structurally inconsistent
*/
public Transaction takerSignsDepositTx(boolean takerIsSeller,
@ -587,9 +589,9 @@ public class TradeWalletService {
checkArgument(!sellerInputs.isEmpty());
// Check if maker's MultiSig script is identical to the takers
Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey);
if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(p2SHMultiSigOutputScript)) {
throw new TransactionVerificationException("Maker's p2SHMultiSigOutputScript does not match to takers p2SHMultiSigOutputScript");
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(hashedMultiSigOutputScript)) {
throw new TransactionVerificationException("Maker's hashedMultiSigOutputScript does not match to takers hashedMultiSigOutputScript");
}
// The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new
@ -601,8 +603,12 @@ public class TradeWalletService {
// We grab the signature from the makersDepositTx and apply it to the new tx input
for (int i = 0; i < buyerInputs.size(); i++) {
TransactionInput makersInput = makersDepositTx.getInputs().get(i);
byte[] makersScriptSigProgram = getMakersScriptSigProgram(makersInput);
byte[] makersScriptSigProgram = makersInput.getScriptSig().getProgram();
TransactionInput input = getTransactionInput(depositTx, makersScriptSigProgram, buyerInputs.get(i));
Script scriptPubKey = checkNotNull(input.getConnectedOutput()).getScriptPubKey();
if (makersScriptSigProgram.length == 0 && !ScriptPattern.isP2WH(scriptPubKey)) {
throw new TransactionVerificationException("Non-segwit inputs from maker not signed.");
}
if (!TransactionWitness.EMPTY.equals(makersInput.getWitness())) {
input.setWitness(makersInput.getWitness());
}
@ -683,6 +689,21 @@ public class TradeWalletService {
}
public void sellerAddsBuyerWitnessesToDepositTx(Transaction myDepositTx,
Transaction buyersDepositTxWithWitnesses) {
int numberInputs = myDepositTx.getInputs().size();
for (int i = 0; i < numberInputs; i++) {
var txInput = myDepositTx.getInput(i);
var witnessFromBuyer = buyersDepositTxWithWitnesses.getInput(i).getWitness();
if (TransactionWitness.EMPTY.equals(txInput.getWitness()) &&
!TransactionWitness.EMPTY.equals(witnessFromBuyer)) {
txInput.setWitness(witnessFromBuyer);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Delayed payout tx
///////////////////////////////////////////////////////////////////////////////////////////
@ -692,11 +713,11 @@ public class TradeWalletService {
Coin minerFee,
long lockTime)
throws AddressFormatException, TransactionVerificationException {
TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0);
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
Transaction delayedPayoutTx = new Transaction(params);
delayedPayoutTx.addInput(p2SHMultiSigOutput);
delayedPayoutTx.addInput(hashedMultiSigOutput);
applyLockTime(lockTime, delayedPayoutTx);
Coin outputAmount = p2SHMultiSigOutput.getValue().subtract(minerFee);
Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee);
delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString));
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
WalletService.verifyTransaction(delayedPayoutTx);
@ -704,13 +725,17 @@ public class TradeWalletService {
}
public byte[] signDelayedPayoutTx(Transaction delayedPayoutTx,
Transaction preparedDepositTx,
DeterministicKey myMultiSigKeyPair,
byte[] buyerPubKey,
byte[] sellerPubKey)
throws AddressFormatException, TransactionVerificationException {
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
Sha256Hash sigHash = delayedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
Sha256Hash sigHash;
Coin delayedPayoutTxInputValue = preparedDepositTx.getOutput(0).getValue();
sigHash = delayedPayoutTx.hashForWitnessSignature(0, redeemScript,
delayedPayoutTxInputValue, Transaction.SigHash.ALL, false);
checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null");
if (myMultiSigKeyPair.isEncrypted()) {
checkNotNull(aesKey);
@ -722,24 +747,45 @@ public class TradeWalletService {
return mySignature.encodeToDER();
}
public Transaction finalizeUnconnectedDelayedPayoutTx(Transaction delayedPayoutTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature,
Coin inputValue)
throws AddressFormatException, TransactionVerificationException, SignatureDecodeException {
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
TransactionInput input = delayedPayoutTx.getInput(0);
input.setScriptSig(ScriptBuilder.createEmpty());
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
input.setWitness(witness);
WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx);
WalletService.verifyTransaction(delayedPayoutTx);
if (checkNotNull(inputValue).isLessThan(delayedPayoutTx.getOutputSum().add(MIN_DELAYED_PAYOUT_TX_FEE))) {
throw new TransactionVerificationException("Delayed payout tx is paying less than the minimum allowed tx fee");
}
Script scriptPubKey = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
input.getScriptSig().correctlySpends(delayedPayoutTx, 0, witness, inputValue, scriptPubKey, Script.ALL_VERIFY_FLAGS);
return delayedPayoutTx;
}
public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx,
byte[] buyerPubKey,
byte[] sellerPubKey,
byte[] buyerSignature,
byte[] sellerSignature)
throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException {
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
TransactionInput input = delayedPayoutTx.getInput(0);
input.setScriptSig(inputScript);
WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx);
WalletService.verifyTransaction(delayedPayoutTx);
finalizeUnconnectedDelayedPayoutTx(delayedPayoutTx, buyerPubKey, sellerPubKey, buyerSignature, sellerSignature, input.getValue());
WalletService.checkWalletConsistency(wallet);
WalletService.checkScriptSig(delayedPayoutTx, input, 0);
checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null");
input.verify(input.getConnectedOutput());
return delayedPayoutTx;
@ -779,7 +825,15 @@ public class TradeWalletService {
// MS redeemScript
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
// MS output from prev. tx is index 0
Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
Sha256Hash sigHash;
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
if (ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey())) {
sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
} else {
Coin inputValue = hashedMultiSigOutput.getValue();
sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript,
inputValue, Transaction.SigHash.ALL, false);
}
checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null");
if (multiSigKeyPair.isEncrypted()) {
checkNotNull(aesKey);
@ -822,7 +876,16 @@ public class TradeWalletService {
// MS redeemScript
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
// MS output from prev. tx is index 0
Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
Sha256Hash sigHash;
if (hashedMultiSigOutputIsLegacy) {
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
} else {
Coin inputValue = hashedMultiSigOutput.getValue();
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
inputValue, Transaction.SigHash.ALL, false);
}
checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null");
if (multiSigKeyPair.isEncrypted()) {
checkNotNull(aesKey);
@ -832,10 +895,16 @@ public class TradeWalletService {
Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerSignature, Transaction.SigHash.ALL, false);
// Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer)
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
redeemScript);
TransactionInput input = payoutTx.getInput(0);
input.setScriptSig(inputScript);
if (hashedMultiSigOutputIsLegacy) {
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
redeemScript);
input.setScriptSig(inputScript);
} else {
input.setScriptSig(ScriptBuilder.createEmpty());
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
input.setWitness(witness);
}
WalletService.printTx("payoutTx", payoutTx);
WalletService.verifyTransaction(payoutTx);
WalletService.checkWalletConsistency(wallet);
@ -863,7 +932,16 @@ public class TradeWalletService {
// MS redeemScript
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
// MS output from prev. tx is index 0
Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
Sha256Hash sigHash;
if (hashedMultiSigOutputIsLegacy) {
sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
} else {
Coin inputValue = hashedMultiSigOutput.getValue();
sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript,
inputValue, Transaction.SigHash.ALL, false);
}
checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null");
if (myMultiSigKeyPair.isEncrypted()) {
checkNotNull(aesKey);
@ -895,9 +973,18 @@ public class TradeWalletService {
TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature),
Transaction.SigHash.ALL, false);
// Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer)
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
TransactionInput input = payoutTx.getInput(0);
input.setScriptSig(inputScript);
if (hashedMultiSigOutputIsLegacy) {
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
redeemScript);
input.setScriptSig(inputScript);
} else {
input.setScriptSig(ScriptBuilder.createEmpty());
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
input.setWitness(witness);
}
WalletService.printTx("mediated payoutTx", payoutTx);
WalletService.verifyTransaction(payoutTx);
WalletService.checkWalletConsistency(wallet);
@ -945,9 +1032,9 @@ public class TradeWalletService {
byte[] arbitratorPubKey)
throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException {
Transaction depositTx = new Transaction(params, depositTxSerialized);
TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0);
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
Transaction payoutTx = new Transaction(params);
payoutTx.addInput(p2SHMultiSigOutput);
payoutTx.addInput(hashedMultiSigOutput);
if (buyerPayoutAmount.isPositive()) {
payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));
}
@ -957,7 +1044,15 @@ public class TradeWalletService {
// take care of sorting!
Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey);
Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
Sha256Hash sigHash;
boolean hashedMultiSigOutputIsLegacy = !ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
if (hashedMultiSigOutputIsLegacy) {
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
} else {
Coin inputValue = hashedMultiSigOutput.getValue();
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
inputValue, Transaction.SigHash.ALL, false);
}
checkNotNull(tradersMultiSigKeyPair, "tradersMultiSigKeyPair must not be null");
if (tradersMultiSigKeyPair.isEncrypted()) {
checkNotNull(aesKey);
@ -966,11 +1061,18 @@ public class TradeWalletService {
TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false);
TransactionSignature arbitratorTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(arbitratorSignature),
Transaction.SigHash.ALL, false);
// Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer)
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(arbitratorTxSig, tradersTxSig),
redeemScript);
TransactionInput input = payoutTx.getInput(0);
input.setScriptSig(inputScript);
// Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer)
if (hashedMultiSigOutputIsLegacy) {
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(
ImmutableList.of(arbitratorTxSig, tradersTxSig),
redeemScript);
input.setScriptSig(inputScript);
} else {
input.setScriptSig(ScriptBuilder.createEmpty());
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, arbitratorTxSig, tradersTxSig);
input.setWitness(witness);
}
WalletService.printTx("disputed payoutTx", payoutTx);
WalletService.verifyTransaction(payoutTx);
WalletService.checkWalletConsistency(wallet);
@ -995,21 +1097,23 @@ public class TradeWalletService {
String sellerPrivateKeyAsHex,
String buyerPubKeyAsHex,
String sellerPubKeyAsHex,
boolean hashedMultiSigOutputIsLegacy,
TxBroadcaster.Callback callback)
throws AddressFormatException, TransactionVerificationException, WalletException {
byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey();
byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey();
Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey);
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey,
hashedMultiSigOutputIsLegacy);
Coin msOutput = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee);
TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, null, msOutput, p2SHMultiSigOutputScript.getProgram());
Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee);
TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, null, msOutputValue, hashedMultiSigOutputScript.getProgram());
Transaction depositTx = new Transaction(params);
depositTx.addOutput(p2SHMultiSigOutput);
depositTx.addOutput(hashedMultiSigOutput);
Transaction payoutTx = new Transaction(params);
Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex);
payoutTx.addInput(new TransactionInput(params, depositTx, p2SHMultiSigOutputScript.getProgram(), new TransactionOutPoint(params, 0, spendTxHash), msOutput));
payoutTx.addInput(new TransactionInput(params, depositTx, null, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue));
if (buyerPayoutAmount.isPositive()) {
payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));
@ -1020,7 +1124,14 @@ public class TradeWalletService {
// take care of sorting!
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
Sha256Hash sigHash;
if (hashedMultiSigOutputIsLegacy) {
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
} else {
Coin inputValue = msOutputValue;
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
inputValue, Transaction.SigHash.ALL, false);
}
ECKey buyerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(buyerPrivateKeyAsHex));
checkNotNull(buyerPrivateKey, "key must not be null");
@ -1032,10 +1143,18 @@ public class TradeWalletService {
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
TransactionInput input = payoutTx.getInput(0);
input.setScriptSig(inputScript);
if (hashedMultiSigOutputIsLegacy) {
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
redeemScript);
input.setScriptSig(inputScript);
} else {
input.setScriptSig(ScriptBuilder.createEmpty());
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
input.setWitness(witness);
}
WalletService.printTx("payoutTx", payoutTx);
WalletService.verifyTransaction(payoutTx);
WalletService.checkWalletConsistency(wallet);
@ -1092,28 +1211,32 @@ public class TradeWalletService {
"input.getConnectedOutput().getParentTransaction() must not be null");
checkNotNull(input.getValue(), "input.getValue() must not be null");
// bitcoinSerialize(false) is used just in case the serialized tx is parsed by a bisq node still using
// bitcoinj 0.14. This is not supposed to happen ever since Version.TRADE_PROTOCOL_VERSION was set to 3,
// but it costs nothing to be on the safe side.
// The serialized tx is just used to obtain its hash, so the witness data is not relevant.
return new RawTransactionInput(input.getOutpoint().getIndex(),
input.getConnectedOutput().getParentTransaction().bitcoinSerialize(false),
input.getValue().value);
}
private byte[] getMakersScriptSigProgram(TransactionInput transactionInput) throws TransactionVerificationException {
byte[] scriptProgram = transactionInput.getScriptSig().getProgram();
if (scriptProgram.length == 0) {
throw new TransactionVerificationException("Inputs from maker not signed.");
}
return scriptProgram;
}
private TransactionInput getTransactionInput(Transaction depositTx,
byte[] scriptProgram,
RawTransactionInput rawTransactionInput) {
return new TransactionInput(params, depositTx, scriptProgram, new TransactionOutPoint(params,
rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)),
return new TransactionInput(params, depositTx, scriptProgram, getConnectedOutPoint(rawTransactionInput),
Coin.valueOf(rawTransactionInput.value));
}
private TransactionOutPoint getConnectedOutPoint(RawTransactionInput rawTransactionInput) {
return new TransactionOutPoint(params, rawTransactionInput.index,
new Transaction(params, rawTransactionInput.parentTransaction));
}
public boolean isP2WH(RawTransactionInput rawTransactionInput) {
return ScriptPattern.isP2WH(
checkNotNull(getConnectedOutPoint(rawTransactionInput).getConnectedOutput()).getScriptPubKey());
}
// TODO: Once we have removed legacy arbitrator from dispute domain we can remove that method as well.
// Atm it is still used by traderSignAndFinalizeDisputedPayoutTx which is used by ArbitrationManager.
@ -1144,8 +1267,13 @@ public class TradeWalletService {
return ScriptBuilder.createMultiSigOutputScript(2, keys);
}
private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey) {
return ScriptBuilder.createP2SHOutputScript(get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey));
private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, boolean legacy) {
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
if (legacy) {
return ScriptBuilder.createP2SHOutputScript(redeemScript);
} else {
return ScriptBuilder.createP2WSHOutputScript(redeemScript);
}
}
private Transaction createPayoutTx(Transaction depositTx,
@ -1153,9 +1281,9 @@ public class TradeWalletService {
Coin sellerPayoutAmount,
String buyerAddressString,
String sellerAddressString) throws AddressFormatException {
TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0);
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
Transaction transaction = new Transaction(params);
transaction.addInput(p2SHMultiSigOutput);
transaction.addInput(hashedMultiSigOutput);
if (buyerPayoutAmount.isPositive()) {
transaction.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));
}
@ -1187,13 +1315,10 @@ public class TradeWalletService {
input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey));
}
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) {
// TODO: Consider using this alternative way to build the scriptCode (taken from bitcoinj master)
// Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey)
Script scriptCode = new ScriptBuilder().data(
ScriptBuilder.createOutputScript(LegacyAddress.fromKey(transaction.getParams(), sigKey)).getProgram())
.build();
// scriptCode is expected to have the format of a legacy P2PKH output script
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey);
Coin value = input.getValue();
TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, scriptCode, value,
TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, aesKey, scriptCode, value,
Transaction.SigHash.ALL, false);
input.setScriptSig(ScriptBuilder.createEmpty());
input.setWitness(TransactionWitness.redeemP2WPKH(txSig, sigKey));

View file

@ -37,7 +37,6 @@ import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
@ -325,13 +324,10 @@ public abstract class WalletService {
}
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) {
try {
// TODO: Consider using this alternative way to build the scriptCode (taken from bitcoinj master)
// Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key);
Script scriptCode = new ScriptBuilder().data(
ScriptBuilder.createOutputScript(LegacyAddress.fromKey(tx.getParams(), key)).getProgram())
.build();
// scriptCode is expected to have the format of a legacy P2PKH output script
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key);
Coin value = txIn.getValue();
TransactionSignature txSig = tx.calculateWitnessSignature(index, key, scriptCode, value,
TransactionSignature txSig = tx.calculateWitnessSignature(index, key, aesKey, scriptCode, value,
Transaction.SigHash.ALL, false);
txIn.setScriptSig(ScriptBuilder.createEmpty());
txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key));
@ -479,10 +475,10 @@ public abstract class WalletService {
return getBalanceForAddress(getAddressFromOutput(output));
}
public Coin getTxFeeForWithdrawalPerByte() {
public Coin getTxFeeForWithdrawalPerVbyte() {
Coin fee = (preferences.isUseCustomWithdrawalTxFee()) ?
Coin.valueOf(preferences.getWithdrawalTxFeeInBytes()) :
feeService.getTxFeePerByte();
Coin.valueOf(preferences.getWithdrawalTxFeeInVbytes()) :
feeService.getTxFeePerVbyte();
log.info("tx fee = " + fee.toFriendlyString());
return fee;
}
@ -521,7 +517,7 @@ public abstract class WalletService {
throws InsufficientMoneyException, AddressFormatException {
SendRequest sendRequest = SendRequest.emptyWallet(Address.fromString(params, toAddress));
sendRequest.fee = Coin.ZERO;
sendRequest.feePerKb = getTxFeeForWithdrawalPerByte().multiply(1000);
sendRequest.feePerKb = getTxFeeForWithdrawalPerVbyte().multiply(1000);
sendRequest.aesKey = aesKey;
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
printTx("empty btc wallet", sendResult.tx);

View file

@ -382,9 +382,9 @@ public class DaoFacade implements DaoSetupService {
return BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight());
}
public Tuple2<Coin, Integer> getBlindVoteMiningFeeAndTxSize(Coin stake)
public Tuple2<Coin, Integer> getBlindVoteMiningFeeAndTxVsize(Coin stake)
throws WalletException, InsufficientMoneyException, TransactionVerificationException {
return myBlindVoteListService.getMiningFeeAndTxSize(stake);
return myBlindVoteListService.getMiningFeeAndTxVsize(stake);
}
// Publish blindVote tx and broadcast blindVote to p2p network and store to blindVoteList.
@ -532,12 +532,12 @@ public class DaoFacade implements DaoSetupService {
lockupTxService.publishLockupTx(lockupAmount, lockTime, lockupReason, hash, resultHandler, exceptionHandler);
}
public Tuple2<Coin, Integer> getLockupTxMiningFeeAndTxSize(Coin lockupAmount,
int lockTime,
LockupReason lockupReason,
byte[] hash)
public Tuple2<Coin, Integer> getLockupTxMiningFeeAndTxVsize(Coin lockupAmount,
int lockTime,
LockupReason lockupReason,
byte[] hash)
throws InsufficientMoneyException, IOException, TransactionVerificationException, WalletException {
return lockupTxService.getMiningFeeAndTxSize(lockupAmount, lockTime, lockupReason, hash);
return lockupTxService.getMiningFeeAndTxVsize(lockupAmount, lockTime, lockupReason, hash);
}
public void publishUnlockTx(String lockupTxId, Consumer<String> resultHandler,
@ -545,9 +545,9 @@ public class DaoFacade implements DaoSetupService {
unlockTxService.publishUnlockTx(lockupTxId, resultHandler, exceptionHandler);
}
public Tuple2<Coin, Integer> getUnlockTxMiningFeeAndTxSize(String lockupTxId)
public Tuple2<Coin, Integer> getUnlockTxMiningFeeAndTxVsize(String lockupTxId)
throws InsufficientMoneyException, TransactionVerificationException, WalletException {
return unlockTxService.getMiningFeeAndTxSize(lockupTxId);
return unlockTxService.getMiningFeeAndTxVsize(lockupTxId);
}
public long getTotalLockupAmount() {

View file

@ -189,14 +189,14 @@ public class MyBlindVoteListService implements PersistedDataHost, DaoStateListen
// API
///////////////////////////////////////////////////////////////////////////////////////////
public Tuple2<Coin, Integer> getMiningFeeAndTxSize(Coin stake)
public Tuple2<Coin, Integer> getMiningFeeAndTxVsize(Coin stake)
throws InsufficientMoneyException, WalletException, TransactionVerificationException {
// We set dummy opReturn data
Coin blindVoteFee = BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight());
Transaction dummyTx = getBlindVoteTx(stake, blindVoteFee, new byte[22]);
Coin miningFee = dummyTx.getFee();
int txSize = dummyTx.bitcoinSerialize().length;
return new Tuple2<>(miningFee, txSize);
int txVsize = dummyTx.getVsize();
return new Tuple2<>(miningFee, txVsize);
}
public void publishBlindVote(Coin stake, ResultHandler resultHandler, ExceptionHandler exceptionHandler) {

View file

@ -91,12 +91,12 @@ public class LockupTxService {
}
}
public Tuple2<Coin, Integer> getMiningFeeAndTxSize(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash)
public Tuple2<Coin, Integer> getMiningFeeAndTxVsize(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash)
throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException {
Transaction tx = getLockupTx(lockupAmount, lockTime, lockupReason, hash);
Coin miningFee = tx.getFee();
int txSize = tx.bitcoinSerialize().length;
return new Tuple2<>(miningFee, txSize);
int txVsize = tx.getVsize();
return new Tuple2<>(miningFee, txVsize);
}
private Transaction getLockupTx(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash)

View file

@ -89,12 +89,12 @@ public class UnlockTxService {
}
}
public Tuple2<Coin, Integer> getMiningFeeAndTxSize(String lockupTxId)
public Tuple2<Coin, Integer> getMiningFeeAndTxVsize(String lockupTxId)
throws InsufficientMoneyException, WalletException, TransactionVerificationException {
Transaction tx = getUnlockTx(lockupTxId);
Coin miningFee = tx.getFee();
int txSize = tx.bitcoinSerialize().length;
return new Tuple2<>(miningFee, txSize);
int txVsize = tx.getVsize();
return new Tuple2<>(miningFee, txVsize);
}
private Transaction getUnlockTx(String lockupTxId)

View file

@ -319,6 +319,24 @@ public class CurrencyUtil {
return currencies;
}
public static List<TradeCurrency> getAllAmazonGiftCardCurrencies() {
List<TradeCurrency> currencies = new ArrayList<>(Arrays.asList(
new FiatCurrency("AUD"),
new FiatCurrency("CAD"),
new FiatCurrency("EUR"),
new FiatCurrency("GBP"),
new FiatCurrency("INR"),
new FiatCurrency("JPY"),
new FiatCurrency("SAR"),
new FiatCurrency("SEK"),
new FiatCurrency("SGD"),
new FiatCurrency("TRY"),
new FiatCurrency("USD")
));
currencies.sort(Comparator.comparing(TradeCurrency::getCode));
return currencies;
}
// https://www.revolut.com/help/getting-started/exchanging-currencies/what-fiat-currencies-are-supported-for-holding-and-exchange
public static List<TradeCurrency> getAllRevolutCurrencies() {
ArrayList<TradeCurrency> currencies = new ArrayList<>(Arrays.asList(

View file

@ -41,7 +41,9 @@ public class LanguageUtil {
"vi", // Vietnamese
"th", // Thai
"ja", // Japanese
"fa" // Persian
"fa", // Persian
"it", // Italian
"cs" // Czech
/*
// not translated yet
"el", // Greek
@ -49,7 +51,6 @@ public class LanguageUtil {
"hu", // Hungarian
"ro", // Romanian
"tr" // Turkish
"it", // Italian
"iw", // Hebrew
"hi", // Hindi
"ko", // Korean
@ -77,7 +78,6 @@ public class LanguageUtil {
"ms", // Malay
"is", // Icelandic
"et", // Estonian
"cs", // Czech
"ar", // Arabic
"vi", // Vietnamese
"th", // Thai

View file

@ -163,7 +163,7 @@ public class CreateOfferService {
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
double sellerSecurityDeposit = getSellerSecurityDepositAsDouble(buyerSecurityDepositAsDouble);
Coin txFeeFromFeeService = getEstimatedFeeAndTxSize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first;
Coin txFeeFromFeeService = getEstimatedFeeAndTxVsize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first;
Coin txFeeToUse = txFee.isPositive() ? txFee : txFeeFromFeeService;
Coin makerFeeAsCoin = offerUtil.getMakerFee(amount);
boolean isCurrencyForMakerFeeBtc = offerUtil.isCurrencyForMakerFeeBtc(amount);
@ -233,15 +233,15 @@ public class CreateOfferService {
return offer;
}
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSize(Coin amount,
OfferPayload.Direction direction,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsize(Coin amount,
OfferPayload.Direction direction,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
Coin reservedFundsForOffer = getReservedFundsForOffer(direction,
amount,
buyerSecurityDeposit,
sellerSecurityDeposit);
return txFeeEstimationService.getEstimatedFeeAndTxSizeForMaker(reservedFundsForOffer,
return txFeeEstimationService.getEstimatedFeeAndTxVsizeForMaker(reservedFundsForOffer,
offerUtil.getMakerFee(amount));
}

View file

@ -177,8 +177,8 @@ public class OfferUtil {
return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount);
}
public Coin getTxFeeBySize(Coin txFeePerByteFromFeeService, int sizeInBytes) {
return txFeePerByteFromFeeService.multiply(getAverageTakerFeeTxSize(sizeInBytes));
public Coin getTxFeeByVsize(Coin txFeePerVbyteFromFeeService, int vsizeInVbytes) {
return txFeePerVbyteFromFeeService.multiply(getAverageTakerFeeTxVsize(vsizeInVbytes));
}
// We use the sum of the size of the trade fee and the deposit tx to get an average.
@ -186,8 +186,8 @@ public class OfferUtil {
// enough. With that we avoid that we overpay in case that the trade fee has many
// inputs and we would apply that fee for the other 2 txs as well. We still might
// overpay a bit for the payout tx.
public int getAverageTakerFeeTxSize(int txSize) {
return (txSize + 320) / 2;
public int getAverageTakerFeeTxVsize(int txVsize) {
return (txVsize + 233) / 2;
}
/**

View file

@ -77,10 +77,9 @@ public class TakeOfferModel implements Model {
private Coin securityDeposit;
private boolean useSavingsWallet;
// 260 kb is typical trade fee tx size with 1 input, but trade tx (deposit + payout)
// are larger so we adjust to 320.
private final int feeTxSize = 320;
private Coin txFeePerByteFromFeeService;
// Use an average of a typical trade fee tx with 1 input, deposit tx and payout tx.
private final int feeTxVsize = 192; // (175+233+169)/3
private Coin txFeePerVbyteFromFeeService;
@Getter
private Coin txFeeFromFeeService;
@Getter
@ -150,26 +149,26 @@ public class TakeOfferModel implements Model {
// payout tx with different fees might be an option but RBF is not supported yet
// in BitcoinJ and batched txs would add more complexity to the trade protocol.
// A typical trade fee tx has about 260 bytes (if one input). The trade txs has
// about 336-414 bytes. We use 320 as a average value.
// A typical trade fee tx has about 175 vbytes (if one input). The trade txs has
// about 169-263 vbytes. We use 192 as a average value.
// Fee calculations:
// Trade fee tx: 260 bytes (1 input)
// Deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes
// Trade fee tx: 175 vbytes (1 input)
// Deposit tx: 233 vbytes (1 MS output+ OP_RETURN) - 263 vbytes
// (1 MS output + OP_RETURN + change in case of smaller trade amount)
// Payout tx: 371 bytes
// Disputed payout tx: 408 bytes
// Payout tx: 169 vbytes
// Disputed payout tx: 139 vbytes
txFeePerByteFromFeeService = getTxFeePerByte();
txFeeFromFeeService = offerUtil.getTxFeeBySize(txFeePerByteFromFeeService, feeTxSize);
log.info("{} txFeePerByte = {}", feeService.getClass().getSimpleName(), txFeePerByteFromFeeService);
txFeePerVbyteFromFeeService = getTxFeePerVbyte();
txFeeFromFeeService = offerUtil.getTxFeeByVsize(txFeePerVbyteFromFeeService, feeTxVsize);
log.info("{} txFeePerVbyte = {}", feeService.getClass().getSimpleName(), txFeePerVbyteFromFeeService);
}
private Coin getTxFeePerByte() {
private Coin getTxFeePerVbyte() {
try {
CompletableFuture<Void> feeRequestFuture = CompletableFuture.runAsync(feeService::requestFees);
feeRequestFuture.get(); // Block until async fee request is complete.
return feeService.getTxFeePerByte();
return feeService.getTxFeePerVbyte();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException("Could not request fees from fee service.", e);
}
@ -284,7 +283,7 @@ public class TakeOfferModel implements Model {
this.totalAvailableBalance = null;
this.totalToPayAsCoin = null;
this.txFeeFromFeeService = null;
this.txFeePerByteFromFeeService = null;
this.txFeePerVbyteFromFeeService = null;
this.useSavingsWallet = true;
this.volume = null;
}
@ -300,8 +299,8 @@ public class TakeOfferModel implements Model {
", addressEntry=" + addressEntry + "\n" +
", amount=" + amount + "\n" +
", securityDeposit=" + securityDeposit + "\n" +
", feeTxSize=" + feeTxSize + "\n" +
", txFeePerByteFromFeeService=" + txFeePerByteFromFeeService + "\n" +
", feeTxVsize=" + feeTxVsize + "\n" +
", txFeePerVbyteFromFeeService=" + txFeePerVbyteFromFeeService + "\n" +
", txFeeFromFeeService=" + txFeeFromFeeService + "\n" +
", takerFee=" + takerFee + "\n" +
", totalToPayAsCoin=" + totalToPayAsCoin + "\n" +

View file

@ -0,0 +1,46 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.payment;
import bisq.core.payment.payload.AmazonGiftCardAccountPayload;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.payment.payload.PaymentMethod;
public final class AmazonGiftCardAccount extends PaymentAccount {
public AmazonGiftCardAccount() {
super(PaymentMethod.AMAZON_GIFT_CARD);
}
@Override
protected PaymentAccountPayload createPayload() {
return new AmazonGiftCardAccountPayload(paymentMethod.getId(), id);
}
public String getEmailOrMobileNr() {
return getAmazonGiftCardAccountPayload().getEmailOrMobileNr();
}
public void setEmailOrMobileNr(String emailOrMobileNr) {
getAmazonGiftCardAccountPayload().setEmailOrMobileNr(emailOrMobileNr);
}
private AmazonGiftCardAccountPayload getAmazonGiftCardAccountPayload() {
return (AmazonGiftCardAccountPayload) paymentAccountPayload;
}
}

View file

@ -80,6 +80,8 @@ public class PaymentAccountFactory {
return new AdvancedCashAccount();
case PaymentMethod.TRANSFERWISE_ID:
return new TransferwiseAccount();
case PaymentMethod.AMAZON_GIFT_CARD_ID:
return new AmazonGiftCardAccount();
case PaymentMethod.BLOCK_CHAINS_INSTANT_ID:
return new InstantCryptoCurrencyAccount();

View file

@ -0,0 +1,103 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.payment.payload;
import bisq.core.locale.Res;
import com.google.protobuf.Message;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@ToString
@Setter
@Getter
@Slf4j
public class AmazonGiftCardAccountPayload extends PaymentAccountPayload {
private String emailOrMobileNr;
public AmazonGiftCardAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private AmazonGiftCardAccountPayload(String paymentMethodName,
String id,
String emailOrMobileNr,
long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) {
super(paymentMethodName,
id,
maxTradePeriod,
excludeFromJsonDataMap);
this.emailOrMobileNr = emailOrMobileNr;
}
@Override
public Message toProtoMessage() {
protobuf.AmazonGiftCardAccountPayload.Builder builder =
protobuf.AmazonGiftCardAccountPayload.newBuilder()
.setEmailOrMobileNr(emailOrMobileNr);
return getPaymentAccountPayloadBuilder()
.setAmazonGiftCardAccountPayload(builder)
.build();
}
public static PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
protobuf.AmazonGiftCardAccountPayload amazonGiftCardAccountPayload = proto.getAmazonGiftCardAccountPayload();
return new AmazonGiftCardAccountPayload(proto.getPaymentMethodId(),
proto.getId(),
amazonGiftCardAccountPayload.getEmailOrMobileNr(),
proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public String getPaymentDetails() {
return Res.get(paymentMethodId) + " - " + getPaymentDetailsForTradePopup().replace("\n", ", ");
}
@Override
public String getPaymentDetailsForTradePopup() {
return Res.getWithCol("payment.email.mobile") + " " + emailOrMobileNr;
}
@Override
public byte[] getAgeWitnessInputData() {
String data = "AmazonGiftCard" + emailOrMobileNr;
return super.getAgeWitnessInputData(data.getBytes(StandardCharsets.UTF_8));
}
}

View file

@ -93,6 +93,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
public static final String PROMPT_PAY_ID = "PROMPT_PAY";
public static final String ADVANCED_CASH_ID = "ADVANCED_CASH";
public static final String TRANSFERWISE_ID = "TRANSFERWISE";
public static final String AMAZON_GIFT_CARD_ID = "AMAZON_GIFT_CARD";
public static final String BLOCK_CHAINS_INSTANT_ID = "BLOCK_CHAINS_INSTANT";
// Cannot be deleted as it would break old trade history entries
@ -132,6 +133,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
public static PaymentMethod PROMPT_PAY;
public static PaymentMethod ADVANCED_CASH;
public static PaymentMethod TRANSFERWISE;
public static PaymentMethod AMAZON_GIFT_CARD;
public static PaymentMethod BLOCK_CHAINS_INSTANT;
// Cannot be deleted as it would break old trade history entries
@ -176,6 +178,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
HAL_CASH = new PaymentMethod(HAL_CASH_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK),
F2F = new PaymentMethod(F2F_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK),
AMAZON_GIFT_CARD = new PaymentMethod(AMAZON_GIFT_CARD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),
// Trans national
UPHOLD = new PaymentMethod(UPHOLD_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK),

View file

@ -23,6 +23,7 @@ import bisq.core.dao.governance.blindvote.storage.BlindVotePayload;
import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload;
import bisq.core.payment.payload.AdvancedCashAccountPayload;
import bisq.core.payment.payload.AliPayAccountPayload;
import bisq.core.payment.payload.AmazonGiftCardAccountPayload;
import bisq.core.payment.payload.AustraliaPayidPayload;
import bisq.core.payment.payload.CashAppAccountPayload;
import bisq.core.payment.payload.CashDepositAccountPayload;
@ -151,6 +152,8 @@ public class CoreProtoResolver implements ProtoResolver {
return AdvancedCashAccountPayload.fromProto(proto);
case TRANSFERWISE_ACCOUNT_PAYLOAD:
return TransferwiseAccountPayload.fromProto(proto);
case AMAZON_GIFT_CARD_ACCOUNT_PAYLOAD:
return AmazonGiftCardAccountPayload.fromProto(proto);
case INSTANT_CRYPTO_CURRENCY_ACCOUNT_PAYLOAD:
return InstantCryptoCurrencyPayload.fromProto(proto);

View file

@ -43,6 +43,7 @@ import bisq.core.trade.statistics.TradeStatistics3Store;
import bisq.core.user.PreferencesPayload;
import bisq.core.user.UserPayload;
import bisq.network.p2p.MailboxMessageList;
import bisq.network.p2p.peers.peerexchange.PeerList;
import bisq.network.p2p.storage.persistence.SequenceNumberMap;
@ -129,6 +130,8 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
return SignedWitnessStore.fromProto(proto.getSignedWitnessStore());
case TRADE_STATISTICS3_STORE:
return TradeStatistics3Store.fromProto(proto.getTradeStatistics3Store());
case MAILBOX_MESSAGE_LIST:
return MailboxMessageList.fromProto(proto.getMailboxMessageList(), networkProtoResolver);
default:
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " +

View file

@ -60,7 +60,7 @@ public class FeeService {
// Static
///////////////////////////////////////////////////////////////////////////////////////////
// Miner fees are between 1-600 sat/byte. We try to stay on the safe side. BTC_DEFAULT_TX_FEE is only used if our
// Miner fees are between 1-600 sat/vbyte. We try to stay on the safe side. BTC_DEFAULT_TX_FEE is only used if our
// fee service would not deliver data.
private static final long BTC_DEFAULT_TX_FEE = 50;
private static final long MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN = 2;
@ -94,10 +94,10 @@ public class FeeService {
private final FeeProvider feeProvider;
private final IntegerProperty feeUpdateCounter = new SimpleIntegerProperty(0);
private long txFeePerByte = BTC_DEFAULT_TX_FEE;
private long txFeePerVbyte = BTC_DEFAULT_TX_FEE;
private Map<String, Long> timeStampMap;
private long lastRequest;
private long minFeePerByte;
private long minFeePerVByte;
private long epochInSecondAtLastRequest;
@ -118,7 +118,7 @@ public class FeeService {
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
minFeePerByte = Config.baseCurrencyNetwork().getDefaultMinFeePerByte();
minFeePerVByte = Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte();
requestFees();
@ -150,15 +150,15 @@ public class FeeService {
timeStampMap = result.first;
epochInSecondAtLastRequest = timeStampMap.get("bitcoinFeesTs");
final Map<String, Long> map = result.second;
txFeePerByte = map.get("BTC");
txFeePerVbyte = map.get("BTC");
if (txFeePerByte < minFeePerByte) {
log.warn("The delivered fee per byte is smaller than the min. default fee of 5 sat/byte");
txFeePerByte = minFeePerByte;
if (txFeePerVbyte < minFeePerVByte) {
log.warn("The delivered fee per vbyte is smaller than the min. default fee of 5 sat/vbyte");
txFeePerVbyte = minFeePerVByte;
}
feeUpdateCounter.set(feeUpdateCounter.get() + 1);
log.info("BTC tx fee: txFeePerByte={}", txFeePerByte);
log.info("BTC tx fee: txFeePerVbyte={}", txFeePerVbyte);
if (resultHandler != null)
resultHandler.run();
});
@ -180,12 +180,12 @@ public class FeeService {
}
}
public Coin getTxFee(int sizeInBytes) {
return getTxFeePerByte().multiply(sizeInBytes);
public Coin getTxFee(int vsizeInVbytes) {
return getTxFeePerVbyte().multiply(vsizeInVbytes);
}
public Coin getTxFeePerByte() {
return Coin.valueOf(txFeePerByte);
public Coin getTxFeePerVbyte() {
return Coin.valueOf(txFeePerVbyte);
}
public ReadOnlyIntegerProperty feeUpdateCounterProperty() {
@ -195,7 +195,7 @@ public class FeeService {
public String getFeeTextForDisplay() {
// only show the fee rate if it has been initialized from the service (see feeUpdateCounter)
if (feeUpdateCounter.get() > 0)
return Res.get("mainView.footer.btcFeeRate", txFeePerByte);
return Res.get("mainView.footer.btcFeeRate", txFeePerVbyte);
return "";
}
}

View file

@ -35,6 +35,7 @@ import bisq.core.trade.failed.FailedTradesManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.storage.P2PDataStorage;
@ -66,6 +67,7 @@ public class CorePersistedDataHost {
persistedDataHosts.add(injector.getInstance(RefundDisputeListService.class));
persistedDataHosts.add(injector.getInstance(P2PDataStorage.class));
persistedDataHosts.add(injector.getInstance(PeerManager.class));
persistedDataHosts.add(injector.getInstance(P2PService.class));
if (injector.getInstance(Config.class).daoActivated) {
persistedDataHosts.add(injector.getInstance(BallotListService.class));

View file

@ -177,7 +177,7 @@ public abstract class SupportManager {
requestPersistence();
if (decryptedMessageWithPubKey != null)
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
p2PService.removeMailboxMsg(decryptedMessageWithPubKey);
}
}
@ -314,7 +314,7 @@ public abstract class SupportManager {
log.debug("decryptedMessageWithPubKey.message " + networkEnvelope);
if (networkEnvelope instanceof SupportMessage) {
dispatchMessage((SupportMessage) networkEnvelope);
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
p2PService.removeMailboxMsg(decryptedMessageWithPubKey);
} else if (networkEnvelope instanceof AckMessage) {
onAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey);
}

View file

@ -395,6 +395,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (!storedDisputeOptional.isPresent()) {
disputeList.add(dispute);
trade.setDisputeState(getDisputeStateStartedByPeer());
tradeManager.requestPersistence();
errorMessage = null;
} else {
// valid case if both have opened a dispute and agent was not online.

View file

@ -204,9 +204,10 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) {
trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmount().value);
trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmount().value);
tradeManager.requestPersistence();
trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED);
tradeManager.requestPersistence();
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
@ -243,6 +244,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
DisputeProtocol tradeProtocol = (DisputeProtocol) tradeManager.getTradeProtocol(trade);
trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_ACCEPTED);
tradeManager.requestPersistence();
// If we have not got yet the peers signature we sign and send to the peer our signature.
// Otherwise we sign and complete with the peers signature the payout tx.
@ -265,5 +267,6 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
public void rejectMediationResult(Trade trade) {
trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_REJECTED);
tradeManager.requestPersistence();
}
}

View file

@ -205,6 +205,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED ||
trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) {
trade.setDisputeState(Trade.DisputeState.REFUND_REQUEST_CLOSED);
tradeManager.requestPersistence();
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);

View file

@ -164,5 +164,7 @@ public class TraderChatManager extends SupportManager {
trade.getDate().getTime());
chatMessage.setSystemMessage(true);
trade.getChatMessages().add(chatMessage);
requestPersistence();
}
}

View file

@ -700,10 +700,6 @@ public abstract class Trade implements Tradable, Model {
}
}
public void appendErrorMessage(String msg) {
errorMessage = errorMessage == null ? msg : errorMessage + "\n" + msg;
}
public boolean mediationResultAppliedPenaltyToSeller() {
// If mediated payout is same or more then normal payout we enable otherwise a penalty was applied
// by mediators and we keep the confirm disabled to avoid that the seller can complete the trade
@ -1099,6 +1095,9 @@ public abstract class Trade implements Tradable, Model {
private void setConfirmedState() {
// we only apply the state if we are not already further in the process
if (!isDepositConfirmed()) {
// As setState is called here from the trade itself we cannot trigger a requestPersistence call.
// But as we get setupConfidenceListener called at startup anyway there is no issue if it would not be
// persisted in case the shutdown routine did not persist the trade.
setState(State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN);
}
}

View file

@ -342,11 +342,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void initPersistedTrade(Trade trade) {
initTradeAndProtocol(trade, getTradeProtocol(trade));
trade.updateDepositTxFromWallet();
requestPersistence();
}
private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) {
tradeProtocol.initialize(processModelServiceProvider, this, trade.getOffer());
trade.initialize(processModelServiceProvider);
requestPersistence();
}
public void requestPersistence() {
@ -431,6 +433,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
((TakerProtocol) tradeProtocol).onTakeOffer();
tradeResultHandler.handleResult(trade);
requestPersistence();
}
},
errorMessageHandler);
@ -544,10 +547,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Date halfTradePeriodDate = trade.getHalfTradePeriodDate();
if (maxTradePeriodDate != null && halfTradePeriodDate != null) {
Date now = new Date();
if (now.after(maxTradePeriodDate))
if (now.after(maxTradePeriodDate)) {
trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER);
else if (now.after(halfTradePeriodDate))
requestPersistence();
} else if (now.after(halfTradePeriodDate)) {
trade.setTradePeriodState(Trade.TradePeriodState.SECOND_HALF);
requestPersistence();
}
}
}
});

View file

@ -35,6 +35,9 @@ import java.util.List;
import lombok.extern.slf4j.Slf4j;
//TODO with the redesign of mailbox messages that is not required anymore. We leave it for now as we want to minimize
// changes for the 1.5.0 release but we should clean up afterwards...
/**
* Util for removing pending mailbox messages in case the trade has been closed by the seller after confirming receipt
* and a AckMessage as mailbox message will be sent by the buyer once they go online. In that case the seller's trade
@ -73,8 +76,7 @@ public class CleanupMailboxMessages {
}
private void cleanupMailboxMessages(List<Trade> trades) {
p2PService.getMailboxItemsByUid().values()
.stream().map(P2PService.MailboxItem::getDecryptedMessageWithPubKey)
p2PService.getMailBoxMessages()
.forEach(message -> handleDecryptedMessageWithPubKey(message, trades));
}
@ -102,7 +104,7 @@ public class CleanupMailboxMessages {
private void removeEntryFromMailbox(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) {
log.info("We found a pending mailbox message ({}) for trade {}. As the trade is closed we remove the mailbox message.",
decryptedMessageWithPubKey.getNetworkEnvelope().getClass().getSimpleName(), trade.getId());
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
p2PService.removeMailboxMsg(decryptedMessageWithPubKey);
}
private boolean isMyMessage(TradeMessage message, Trade trade) {

View file

@ -84,13 +84,13 @@ public class ClosedTradableManager implements PersistedDataHost {
public void add(Tradable tradable) {
if (closedTradables.add(tradable)) {
persistenceManager.requestPersistence();
requestPersistence();
}
}
public void remove(Tradable tradable) {
if (closedTradables.remove(tradable)) {
persistenceManager.requestPersistence();
requestPersistence();
}
}
@ -117,4 +117,8 @@ public class ClosedTradableManager implements PersistedDataHost {
return getClosedTrades().stream()
.filter(Trade::isFundsLockedIn);
}
private void requestPersistence() {
persistenceManager.requestPersistence();
}
}

View file

@ -95,13 +95,13 @@ public class FailedTradesManager implements PersistedDataHost {
public void add(Trade trade) {
if (failedTrades.add(trade)) {
persistenceManager.requestPersistence();
requestPersistence();
}
}
public void removeTrade(Trade trade) {
if (failedTrades.remove(trade)) {
persistenceManager.requestPersistence();
requestPersistence();
}
}
@ -129,7 +129,7 @@ public class FailedTradesManager implements PersistedDataHost {
if (unFailTradeCallback.apply(trade)) {
log.info("Unfailing trade {}", trade.getId());
if (failedTrades.remove(trade)) {
persistenceManager.requestPersistence();
requestPersistence();
}
}
}
@ -151,4 +151,8 @@ public class FailedTradesManager implements PersistedDataHost {
}
return blockingTrades.toString();
}
private void requestPersistence() {
persistenceManager.requestPersistence();
}
}

View file

@ -33,16 +33,19 @@ import lombok.Value;
public final class DelayedPayoutTxSignatureRequest extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final byte[] delayedPayoutTx;
private final byte[] delayedPayoutTxSellerSignature;
public DelayedPayoutTxSignatureRequest(String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTx) {
byte[] delayedPayoutTx,
byte[] delayedPayoutTxSellerSignature) {
this(Version.getP2PMessageVersion(),
uid,
tradeId,
senderNodeAddress,
delayedPayoutTx);
delayedPayoutTx,
delayedPayoutTxSellerSignature);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -53,10 +56,12 @@ public final class DelayedPayoutTxSignatureRequest extends TradeMessage implemen
String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTx) {
byte[] delayedPayoutTx,
byte[] delayedPayoutTxSellerSignature) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.delayedPayoutTx = delayedPayoutTx;
this.delayedPayoutTxSellerSignature = delayedPayoutTxSellerSignature;
}
@ -67,16 +72,19 @@ public final class DelayedPayoutTxSignatureRequest extends TradeMessage implemen
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx)))
.setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx))
.setDelayedPayoutTxSellerSignature(ByteString.copyFrom(delayedPayoutTxSellerSignature)))
.build();
}
public static DelayedPayoutTxSignatureRequest fromProto(protobuf.DelayedPayoutTxSignatureRequest proto, int messageVersion) {
public static DelayedPayoutTxSignatureRequest fromProto(protobuf.DelayedPayoutTxSignatureRequest proto,
int messageVersion) {
return new DelayedPayoutTxSignatureRequest(messageVersion,
proto.getUid(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDelayedPayoutTx().toByteArray());
proto.getDelayedPayoutTx().toByteArray(),
proto.getDelayedPayoutTxSellerSignature().toByteArray());
}
@Override
@ -84,6 +92,7 @@ public final class DelayedPayoutTxSignatureRequest extends TradeMessage implemen
return "DelayedPayoutTxSignatureRequest{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n delayedPayoutTx=" + Utilities.bytesAsHexString(delayedPayoutTx) +
",\n delayedPayoutTxSellerSignature=" + Utilities.bytesAsHexString(delayedPayoutTxSellerSignature) +
"\n} " + super.toString();
}
}

View file

@ -32,17 +32,20 @@ import lombok.Value;
@Value
public final class DelayedPayoutTxSignatureResponse extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final byte[] delayedPayoutTxSignature;
private final byte[] delayedPayoutTxBuyerSignature;
private final byte[] depositTx;
public DelayedPayoutTxSignatureResponse(String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTxSignature) {
byte[] delayedPayoutTxBuyerSignature,
byte[] depositTx) {
this(Version.getP2PMessageVersion(),
uid,
tradeId,
senderNodeAddress,
delayedPayoutTxSignature);
delayedPayoutTxBuyerSignature,
depositTx);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -53,10 +56,12 @@ public final class DelayedPayoutTxSignatureResponse extends TradeMessage impleme
String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] delayedPayoutTxSignature) {
byte[] delayedPayoutTxBuyerSignature,
byte[] depositTx) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.delayedPayoutTxSignature = delayedPayoutTxSignature;
this.delayedPayoutTxBuyerSignature = delayedPayoutTxBuyerSignature;
this.depositTx = depositTx;
}
@ -67,24 +72,28 @@ public final class DelayedPayoutTxSignatureResponse extends TradeMessage impleme
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDelayedPayoutTxSignature(ByteString.copyFrom(delayedPayoutTxSignature))
.setDelayedPayoutTxBuyerSignature(ByteString.copyFrom(delayedPayoutTxBuyerSignature))
.setDepositTx(ByteString.copyFrom(depositTx))
)
.build();
}
public static DelayedPayoutTxSignatureResponse fromProto(protobuf.DelayedPayoutTxSignatureResponse proto, int messageVersion) {
public static DelayedPayoutTxSignatureResponse fromProto(protobuf.DelayedPayoutTxSignatureResponse proto,
int messageVersion) {
return new DelayedPayoutTxSignatureResponse(messageVersion,
proto.getUid(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDelayedPayoutTxSignature().toByteArray());
proto.getDelayedPayoutTxBuyerSignature().toByteArray(),
proto.getDepositTx().toByteArray());
}
@Override
public String toString() {
return "DelayedPayoutTxSignatureResponse{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n delayedPayoutTxSignature=" + Utilities.bytesAsHexString(delayedPayoutTxSignature) +
",\n delayedPayoutTxBuyerSignature=" + Utilities.bytesAsHexString(delayedPayoutTxBuyerSignature) +
",\n depositTx=" + Utilities.bytesAsHexString(depositTx) +
"\n} " + super.toString();
}
}

View file

@ -34,17 +34,17 @@ import lombok.Value;
@Value
public final class DepositTxMessage extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final byte[] depositTx;
private final byte[] depositTxWithoutWitnesses;
public DepositTxMessage(String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] depositTx) {
byte[] depositTxWithoutWitnesses) {
this(Version.getP2PMessageVersion(),
uid,
tradeId,
senderNodeAddress,
depositTx);
depositTxWithoutWitnesses);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -55,10 +55,10 @@ public final class DepositTxMessage extends TradeMessage implements DirectMessag
String uid,
String tradeId,
NodeAddress senderNodeAddress,
byte[] depositTx) {
byte[] depositTxWithoutWitnesses) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.depositTx = depositTx;
this.depositTxWithoutWitnesses = depositTxWithoutWitnesses;
}
@Override
@ -68,7 +68,7 @@ public final class DepositTxMessage extends TradeMessage implements DirectMessag
.setUid(uid)
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDepositTx(ByteString.copyFrom(depositTx)))
.setDepositTxWithoutWitnesses(ByteString.copyFrom(depositTxWithoutWitnesses)))
.build();
}
@ -77,14 +77,14 @@ public final class DepositTxMessage extends TradeMessage implements DirectMessag
proto.getUid(),
proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDepositTx().toByteArray());
proto.getDepositTxWithoutWitnesses().toByteArray());
}
@Override
public String toString() {
return "DepositTxMessage{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n depositTx=" + Utilities.bytesAsHexString(depositTx) +
",\n depositTxWithoutWitnesses=" + Utilities.bytesAsHexString(depositTxWithoutWitnesses) +
"\n} " + super.toString();
}
}

View file

@ -26,6 +26,7 @@ import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.buyer.BuyerFinalizesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse;
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener;
@ -103,6 +104,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
MakerRemovesOpenOffer.class,
BuyerVerifiesPreparedDelayedPayoutTx.class,
BuyerSignsDelayedPayoutTx.class,
BuyerFinalizesDelayedPayoutTx.class,
BuyerSendsDelayedPayoutTxSignatureResponse.class)
.withTimeout(30))
.executeTasks();

View file

@ -29,6 +29,7 @@ import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.buyer.BuyerFinalizesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse;
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener;
@ -83,7 +84,10 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
BuyerAsTakerCreatesDepositTxInputs.class,
TakerSendInputsForDepositTxRequest.class)
.withTimeout(30))
.run(() -> processModel.setTempTradingPeerNodeAddress(trade.getTradingPeerNodeAddress()))
.run(() -> {
processModel.setTempTradingPeerNodeAddress(trade.getTradingPeerNodeAddress());
processModel.getTradeManager().requestPersistence();
})
.executeTasks();
}
@ -116,6 +120,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
BuyerProcessDelayedPayoutTxSignatureRequest.class,
BuyerVerifiesPreparedDelayedPayoutTx.class,
BuyerSignsDelayedPayoutTx.class,
BuyerFinalizesDelayedPayoutTx.class,
BuyerSendsDelayedPayoutTxSignatureResponse.class)
.withTimeout(30))
.executeTasks();

View file

@ -147,7 +147,10 @@ public abstract class BuyerProtocol extends DisputeProtocol {
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED))
.run(() -> {
trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED);
processModel.getTradeManager().requestPersistence();
})
.executeTasks();
}

View file

@ -99,11 +99,13 @@ public class FluentProtocol {
NodeAddress peer = condition.getPeer();
if (peer != null) {
tradeProtocol.processModel.setTempTradingPeerNodeAddress(peer);
tradeProtocol.processModel.getTradeManager().requestPersistence();
}
TradeMessage message = condition.getMessage();
if (message != null) {
tradeProtocol.processModel.setTradeMessage(message);
tradeProtocol.processModel.getTradeManager().requestPersistence();
}
TradeTaskRunner taskRunner = setup.getTaskRunner(message, condition.getEvent());

View file

@ -294,6 +294,9 @@ public class ProcessModel implements Model, PersistablePayload {
public void setPaymentStartedMessageState(MessageState paymentStartedMessageStateProperty) {
this.paymentStartedMessageStateProperty.set(paymentStartedMessageStateProperty);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
}
void setDepositTxSentAckMessage(AckMessage ackMessage) {
@ -305,6 +308,9 @@ public class ProcessModel implements Model, PersistablePayload {
public void setDepositTxMessageState(MessageState messageState) {
this.depositTxMessageStateProperty.set(messageState);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
}
void witnessDebugLog(Trade trade) {

View file

@ -35,6 +35,7 @@ import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime;
import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment;
import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepositTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage;
@ -103,6 +104,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
MakerRemovesOpenOffer.class,
SellerAsMakerFinalizesDepositTx.class,
SellerCreatesDelayedPayoutTx.class,
SellerSignsDelayedPayoutTx.class,
SellerSendDelayedPayoutTxSignatureRequest.class)
.withTimeout(30))
.executeTasks();

View file

@ -30,6 +30,7 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs;
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx;
import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx;
@ -98,6 +99,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
TakerPublishFeeTx.class,
SellerAsTakerSignsDepositTx.class,
SellerCreatesDelayedPayoutTx.class,
SellerSignsDelayedPayoutTx.class,
SellerSendDelayedPayoutTxSignatureRequest.class)
.withTimeout(30))
.executeTasks();

View file

@ -33,7 +33,6 @@ import bisq.core.trade.protocol.tasks.seller.SellerPublishesTradeStatistics;
import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSendsDepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.network.p2p.NodeAddress;
@ -77,7 +76,6 @@ public abstract class SellerProtocol extends DisputeProtocol {
.with(message)
.from(peer))
.setup(tasks(SellerProcessDelayedPayoutTxSignatureResponse.class,
SellerSignsDelayedPayoutTx.class,
SellerFinalizesDelayedPayoutTx.class,
SellerSendsDepositTxAndDelayedPayoutTxMessage.class,
SellerPublishesDepositTx.class,
@ -99,7 +97,12 @@ public abstract class SellerProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) {
expect(phase(Trade.Phase.DEPOSIT_CONFIRMED)
// We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case
// that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received
// a mailbox message with CounterCurrencyTransferStartedMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED)
.with(message)
.from(peer)
.preCondition(trade.getPayoutTx() == null,
@ -141,7 +144,10 @@ public abstract class SellerProtocol extends DisputeProtocol {
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT))
.run(() -> {
trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT);
processModel.getTradeManager().requestPersistence();
})
.executeTasks();
}

View file

@ -30,7 +30,6 @@ import bisq.network.p2p.DecryptedDirectMessageListener;
import bisq.network.p2p.DecryptedMessageWithPubKey;
import bisq.network.p2p.MailboxMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendMailboxMessageListener;
import bisq.network.p2p.messaging.DecryptedMailboxListener;
@ -42,6 +41,8 @@ import bisq.common.taskrunner.Task;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@ -77,10 +78,17 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
if (!trade.isWithdrawn()) {
processModel.getP2PService().addDecryptedDirectMessageListener(this);
}
processModel.getP2PService().addDecryptedMailboxListener(this);
processModel.getP2PService().getMailboxItemsByUid().values()
.stream().map(P2PService.MailboxItem::getDecryptedMessageWithPubKey)
.forEach(this::handleDecryptedMessageWithPubKey);
// We delay a bit here as the trade gets updated from the wallet to update the trade
// state (deposit confirmed) and that happens after our method is called.
// TODO To fix that in a better way we would need to change the order of some routines
// from the TradeManager, but as we are close to a release I dont want to risk a bigger
// change and leave that for a later PR
UserThread.runAfter(() -> {
processModel.getP2PService().addDecryptedMailboxListener(this);
processModel.getP2PService().getMailBoxMessages()
.forEach(this::handleDecryptedMessageWithPubKey);
}, 100, TimeUnit.MILLISECONDS);
}
public void onWithdrawCompleted() {
@ -138,7 +146,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// We only remove here if we have already completed the trade.
// Otherwise removal is done after successfully applied the task runner.
if (trade.isWithdrawn()) {
processModel.getP2PService().removeEntryFromMailbox(decryptedMessageWithPubKey);
processModel.getP2PService().removeMailboxMsg(decryptedMessageWithPubKey);
log.info("Remove {} from the P2P network.", tradeMessage.getClass().getSimpleName());
return;
}
@ -152,7 +160,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
onAckMessage((AckMessage) networkEnvelope, peer);
}
// In any case we remove the msg
processModel.getP2PService().removeEntryFromMailbox(decryptedMessageWithPubKey);
processModel.getP2PService().removeMailboxMsg(decryptedMessageWithPubKey);
log.info("Remove {} from the P2P network.", networkEnvelope.getClass().getSimpleName());
}
}
@ -165,7 +173,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
PublicKey sigPubKey = processModel.getTradingPeer().getPubKeyRing().getSignaturePubKey();
// We reconstruct the DecryptedMessageWithPubKey from the message and the peers signature pubKey
DecryptedMessageWithPubKey decryptedMessageWithPubKey = new DecryptedMessageWithPubKey(tradeMessage, sigPubKey);
processModel.getP2PService().removeEntryFromMailbox(decryptedMessageWithPubKey);
processModel.getP2PService().removeMailboxMsg(decryptedMessageWithPubKey);
log.info("Remove {} from the P2P network.", tradeMessage.getClass().getSimpleName());
}
}
@ -299,6 +307,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
log.error("Timeout reached. TradeID={}, state={}, timeoutSec={}",
trade.getId(), trade.stateProperty().get(), timeoutSec);
trade.setErrorMessage("Timeout reached. Protocol did not complete in " + timeoutSec + " sec.");
processModel.getTradeManager().requestPersistence();
cleanup();
}, timeoutSec);
}

View file

@ -52,6 +52,8 @@ public class ProcessPeerPublishedDelayedPayoutTxMessage extends TradeTask {
Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx());
WalletService.maybeAddSelfTxToWallet(delayedPayoutTx, processModel.getBtcWalletService().getWallet());
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -91,6 +91,7 @@ public abstract class SetupPayoutTxListener extends TradeTask {
if (trade.getPayoutTx() == null) {
Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash());
trade.setPayoutTx(walletTx);
processModel.getTradeManager().requestPersistence();
BtcWalletService.printTx("payoutTx received from network", walletTx);
setState();
} else {

View file

@ -37,9 +37,18 @@ public abstract class TradeTask extends Task<Trade> {
processModel = trade.getProcessModel();
}
@Override
protected void complete() {
processModel.getTradeManager().requestPersistence();
super.complete();
}
@Override
protected void failed() {
trade.setErrorMessage(errorMessage);
processModel.getTradeManager().requestPersistence();
super.failed();
}
@ -47,6 +56,8 @@ public abstract class TradeTask extends Task<Trade> {
protected void failed(String message) {
appendToErrorMessage(message);
trade.setErrorMessage(errorMessage);
processModel.getTradeManager().requestPersistence();
super.failed();
}
@ -55,6 +66,8 @@ public abstract class TradeTask extends Task<Trade> {
t.printStackTrace();
appendExceptionToErrorMessage(t);
trade.setErrorMessage(errorMessage);
processModel.getTradeManager().requestPersistence();
super.failed();
}
}

View file

@ -0,0 +1,61 @@
package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Transaction;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class BuyerFinalizesDelayedPayoutTx extends TradeTask {
public BuyerFinalizesDelayedPayoutTx(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
BtcWalletService btcWalletService = processModel.getBtcWalletService();
String id = processModel.getOffer().getId();
Transaction preparedDepositTx = btcWalletService.getTxFromSerializedTx(processModel.getPreparedDepositTx());
Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey();
checkArgument(Arrays.equals(buyerMultiSigPubKey,
btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()),
"buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id);
byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey();
byte[] buyerSignature = processModel.getDelayedPayoutTxSignature();
byte[] sellerSignature = processModel.getTradingPeer().getDelayedPayoutTxSignature();
Transaction signedDelayedPayoutTx = processModel.getTradeWalletService().finalizeUnconnectedDelayedPayoutTx(
preparedDelayedPayoutTx,
buyerMultiSigPubKey,
sellerMultiSigPubKey,
buyerSignature,
sellerSignature,
preparedDepositTx.getOutput(0).getValue());
trade.applyDelayedPayoutTxBytes(signedDelayedPayoutTx.bitcoinSerialize());
log.info("DelayedPayoutTxBytes = {}", Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes()));
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -46,6 +46,7 @@ public class BuyerProcessDelayedPayoutTxSignatureRequest extends TradeTask {
byte[] delayedPayoutTxAsBytes = checkNotNull(request.getDelayedPayoutTx());
Transaction preparedDelayedPayoutTx = processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxAsBytes);
processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx);
processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(request.getDelayedPayoutTxSellerSignature()));
// When we receive that message the taker has published the taker fee, so we apply it to the trade.
// The takerFeeTx was sent in the first message. It should be part of DelayedPayoutTxSignatureRequest
@ -54,6 +55,8 @@ public class BuyerProcessDelayedPayoutTxSignatureRequest extends TradeTask {
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -26,12 +26,16 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.Validator;
import bisq.common.taskrunner.TaskRunner;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.wallet.Wallet;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -59,9 +63,11 @@ public class BuyerProcessDepositTxAndDelayedPayoutTxMessage extends TradeTask {
// To access tx confidence we need to add that tx into our wallet.
byte[] delayedPayoutTxBytes = checkNotNull(message.getDelayedPayoutTx());
checkArgument(Arrays.equals(delayedPayoutTxBytes, trade.getDelayedPayoutTxBytes()),
"mismatch between delayedPayoutTx received from peer and our one." +
"\n Expected: " + Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes()) +
"\n Received: " + Utilities.bytesAsHexString(delayedPayoutTxBytes));
trade.applyDelayedPayoutTxBytes(delayedPayoutTxBytes);
BtcWalletService.printTx("delayedPayoutTx received from peer",
checkNotNull(trade.getDelayedPayoutTx()));
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
@ -73,6 +79,8 @@ public class BuyerProcessDepositTxAndDelayedPayoutTxMessage extends TradeTask {
processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(),
AddressEntry.Context.RESERVED_FOR_TRADE);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -73,6 +73,8 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask {
processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness);
}
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -83,11 +83,15 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
@Override
protected void setStateSent() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG);
processModel.getTradeManager().requestPersistence();
cleanup();
// Complete is called in base class
}
@ -104,6 +108,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
if (!trade.isPayoutPublished()) {
tryToSendAgainLater();
}
processModel.getTradeManager().requestPersistence();
}
// We override the default behaviour for onFault and do not call appendToErrorMessage and failed
@ -118,6 +123,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
if (!trade.isPayoutPublished()) {
tryToSendAgainLater();
}
processModel.getTradeManager().requestPersistence();
}
@Override
@ -173,6 +179,9 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
if (newValue == MessageState.ACKNOWLEDGED) {
// We treat a ACK like BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG);
processModel.getTradeManager().requestPersistence();
cleanup();
complete();
}

View file

@ -44,10 +44,15 @@ public class BuyerSendsDelayedPayoutTxSignatureResponse extends TradeTask {
runInterceptHook();
byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature());
byte[] depositTxBytes = processModel.getDepositTx() != null
? processModel.getDepositTx().bitcoinSerialize() // set in BuyerAsTakerSignsDepositTx task
: processModel.getPreparedDepositTx(); // set in BuyerAsMakerCreatesAndSignsDepositTx task
DelayedPayoutTxSignatureResponse message = new DelayedPayoutTxSignatureResponse(UUID.randomUUID().toString(),
processModel.getOfferId(),
processModel.getMyNodeAddress(),
delayedPayoutTxSignature);
delayedPayoutTxSignature,
depositTxBytes);
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",

View file

@ -100,6 +100,8 @@ public class BuyerSetupDepositTxListener extends TradeTask {
// We don't want to trigger the tradeStateSubscription when setting the state, so we unsubscribe before
unSubscribeAndRemoveListener();
trade.setState(Trade.State.BUYER_SAW_DEPOSIT_TX_IN_NETWORK);
processModel.getTradeManager().requestPersistence();
} else {
unSubscribeAndRemoveListener();
}

View file

@ -45,5 +45,7 @@ public class BuyerSetupPayoutTxListener extends SetupPayoutTxListener {
@Override
protected void setState() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_PAYOUT_TX_IN_NETWORK);
processModel.getTradeManager().requestPersistence();
}
}

View file

@ -81,6 +81,8 @@ public class BuyerSignPayoutTx extends TradeTask {
sellerMultiSigPubKey);
processModel.setPayoutTxSignature(payoutTxSignature);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -24,6 +24,7 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.crypto.DeterministicKey;
@ -46,7 +47,11 @@ public class BuyerSignsDelayedPayoutTx extends TradeTask {
runInterceptHook();
Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
BtcWalletService btcWalletService = processModel.getBtcWalletService();
NetworkParameters params = btcWalletService.getParams();
Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx());
String id = processModel.getOffer().getId();
byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey();
@ -58,11 +63,14 @@ public class BuyerSignsDelayedPayoutTx extends TradeTask {
byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey();
byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx(
preparedDelayedPayoutTx,
preparedDepositTx,
myMultiSigKeyPair,
buyerMultiSigPubKey,
sellerMultiSigPubKey);
processModel.setDelayedPayoutTxSignature(delayedPayoutTxSignature);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -25,6 +25,8 @@ import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
public BuyerVerifiesPreparedDelayedPayoutTx(TaskRunner<Trade> taskHandler, Trade trade) {
@ -36,11 +38,22 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
try {
runInterceptHook();
var preparedDelayedPayoutTx = processModel.getPreparedDelayedPayoutTx();
TradeDataValidation.validateDelayedPayoutTx(trade,
processModel.getPreparedDelayedPayoutTx(),
preparedDelayedPayoutTx,
processModel.getDaoFacade(),
processModel.getBtcWalletService());
// If the deposit tx is non-malleable, we already know its final ID, so should check that now
// before sending any further data to the seller, to provide extra protection for the buyer.
if (isDepositTxNonMalleable()) {
var preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(
processModel.getPreparedDepositTx());
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, checkNotNull(preparedDelayedPayoutTx));
} else {
log.info("Deposit tx is malleable, so we skip preparedDelayedPayoutTx input validation.");
}
complete();
} catch (TradeDataValidation.ValidationException e) {
failed(e.getMessage());
@ -48,4 +61,12 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
failed(t);
}
}
private boolean isDepositTxNonMalleable() {
var buyerInputs = checkNotNull(processModel.getRawTransactionInputs());
var sellerInputs = checkNotNull(processModel.getTradingPeer().getRawTransactionInputs());
return buyerInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH) &&
sellerInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH);
}
}

View file

@ -72,6 +72,8 @@ public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask {
.add(tradeAmount);
List<RawTransactionInput> takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs());
checkArgument(takerRawTransactionInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH),
"all takerRawTransactionInputs must be P2WH");
long takerChangeOutputValue = tradingPeer.getChangeOutputValue();
@Nullable String takerChangeAddressString = tradingPeer.getChangeOutputAddress();
Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress();
@ -97,6 +99,8 @@ public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask {
processModel.setPreparedDepositTx(result.depositTransaction);
processModel.setRawTransactionInputs(result.rawMakerInputs);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -22,6 +22,8 @@ import bisq.core.trade.protocol.tasks.maker.MakerSendsInputsForDepositTxResponse
import bisq.common.taskrunner.TaskRunner;
import org.bitcoinj.core.Transaction;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -32,6 +34,9 @@ public class BuyerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInput
@Override
protected byte[] getPreparedDepositTx() {
return processModel.getPreparedDepositTx();
Transaction preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx());
// Remove witnesses from preparedDepositTx, so that the seller can still compute the final
// tx id, but cannot publish it before providing the buyer with a signed delayed payout tx.
return preparedDepositTx.bitcoinSerialize(false);
}
}

View file

@ -53,6 +53,8 @@ public class BuyerAsTakerCreatesDepositTxInputs extends TradeTask {
processModel.setChangeOutputValue(result.changeOutputValue);
processModel.setChangeOutputAddress(result.changeOutputAddress);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -41,10 +41,12 @@ public class BuyerAsTakerSendsDepositTxMessage extends TradeTask {
try {
runInterceptHook();
if (processModel.getDepositTx() != null) {
// Remove witnesses from the sent depositTx, so that the seller can still compute the final
// tx id, but cannot publish it before providing the buyer with a signed delayed payout tx.
DepositTxMessage message = new DepositTxMessage(UUID.randomUUID().toString(),
processModel.getOfferId(),
processModel.getMyNodeAddress(),
processModel.getDepositTx().bitcoinSerialize());
processModel.getDepositTx().bitcoinSerialize(false));
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",
@ -72,7 +74,7 @@ public class BuyerAsTakerSendsDepositTxMessage extends TradeTask {
}
);
} else {
log.error("processModel.getDepositTx() = " + processModel.getDepositTx());
log.error("processModel.getDepositTx() = {}", processModel.getDepositTx());
failed("DepositTx is null");
}
} catch (Throwable t) {

View file

@ -88,6 +88,8 @@ public class BuyerAsTakerSignsDepositTx extends TradeTask {
sellerMultiSigPubKey);
processModel.setDepositTx(depositTx);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -95,6 +95,8 @@ public class MakerCreateAndSignContract extends TradeTask {
processModel.setMyMultiSigPubKey(makerMultiSigPubKey);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -89,7 +89,7 @@ public abstract class MakerSendsInputsForDepositTxResponse extends TradeTask {
trade.getLockTime());
trade.setState(Trade.State.MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST);
processModel.getTradeManager().requestPersistence();
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
@ -103,6 +103,7 @@ public abstract class MakerSendsInputsForDepositTxResponse extends TradeTask {
log.info("{} arrived at peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
trade.setState(Trade.State.MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST);
processModel.getTradeManager().requestPersistence();
complete();
}
@ -112,6 +113,7 @@ public abstract class MakerSendsInputsForDepositTxResponse extends TradeTask {
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
trade.setState(Trade.State.MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST);
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
processModel.getTradeManager().requestPersistence();
failed(errorMessage);
}
}

View file

@ -47,6 +47,8 @@ public class MakerSetsLockTime extends TradeTask {
log.info("lockTime={}, delay={}", lockTime, delay);
trade.setLockTime(lockTime);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -45,5 +45,6 @@ public class BroadcastMediatedPayoutTx extends BroadcastPayoutTx {
@Override
protected void setState() {
trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED);
processModel.getTradeManager().requestPersistence();
}
}

View file

@ -108,6 +108,8 @@ public class FinalizeMediatedPayoutTx extends TradeTask {
trade.setPayoutTx(transaction);
processModel.getTradeManager().requestPersistence();
walletService.swapTradeEntryToAvailableEntry(tradeId, AddressEntry.Context.MULTI_SIG);
complete();

View file

@ -51,6 +51,8 @@ public class ProcessMediatedPayoutSignatureMessage extends TradeTask {
trade.setMediationResultState(MediationResultState.RECEIVED_SIG_MSG);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -74,6 +74,9 @@ public class ProcessMediatedPayoutTxPublishedMessage extends TradeTask {
} else {
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId());
}
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -60,6 +60,7 @@ public class SendMediatedPayoutSignatureMessage extends TradeTask {
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
trade.setMediationResultState(MediationResultState.SIG_MSG_SENT);
processModel.getTradeManager().requestPersistence();
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
peersPubKeyRing,
message,
@ -70,6 +71,7 @@ public class SendMediatedPayoutSignatureMessage extends TradeTask {
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
trade.setMediationResultState(MediationResultState.SIG_MSG_ARRIVED);
processModel.getTradeManager().requestPersistence();
complete();
}
@ -79,6 +81,7 @@ public class SendMediatedPayoutSignatureMessage extends TradeTask {
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
trade.setMediationResultState(MediationResultState.SIG_MSG_IN_MAILBOX);
processModel.getTradeManager().requestPersistence();
complete();
}
@ -88,6 +91,7 @@ public class SendMediatedPayoutSignatureMessage extends TradeTask {
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
trade.setMediationResultState(MediationResultState.SIG_MSG_SEND_FAILED);
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
processModel.getTradeManager().requestPersistence();
failed(errorMessage);
}
}

View file

@ -54,21 +54,25 @@ public class SendMediatedPayoutTxPublishedMessage extends SendMailboxMessageTask
@Override
protected void setStateSent() {
trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_SENT);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_ARRIVED);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateFault() {
trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED);
processModel.getTradeManager().requestPersistence();
}
@Override

View file

@ -49,5 +49,6 @@ public class SetupMediatedPayoutTxListener extends SetupPayoutTxListener {
if (trade.getPayoutTx() != null) {
processModel.getTradeManager().closeDisputedTrade(trade.getId(), Trade.DisputeState.MEDIATION_CLOSED);
}
processModel.getTradeManager().requestPersistence();
}
}

View file

@ -99,6 +99,8 @@ public class SignMediatedPayoutTx extends TradeTask {
sellerMultiSigPubKey);
processModel.setMediatedPayoutTxSignature(mediatedPayoutTxSignature);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -44,5 +44,6 @@ public class SellerBroadcastPayoutTx extends BroadcastPayoutTx {
@Override
protected void setState() {
trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
processModel.getTradeManager().requestPersistence();
}
}

View file

@ -20,6 +20,7 @@ package bisq.core.trade.protocol.tasks.seller;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.governance.param.Param;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
@ -53,9 +54,15 @@ public class SellerCreatesDelayedPayoutTx extends TradeTask {
donationAddressString,
minerFee,
lockTime);
TradeDataValidation.validateDelayedPayoutTx(trade,
preparedDelayedPayoutTx,
processModel.getDaoFacade(),
processModel.getBtcWalletService());
processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -68,6 +68,8 @@ public class SellerFinalizesDelayedPayoutTx extends TradeTask {
trade.applyDelayedPayoutTx(signedDelayedPayoutTx);
log.info("DelayedPayoutTxBytes = {}", Utilities.bytesAsHexString(trade.getDelayedPayoutTxBytes()));
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -61,6 +61,8 @@ public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTas
trade.setState(Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -42,11 +42,18 @@ public class SellerProcessDelayedPayoutTxSignatureResponse extends TradeTask {
checkNotNull(response);
checkTradeId(processModel.getOfferId(), response);
processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(response.getDelayedPayoutTxSignature()));
processModel.getTradingPeer().setDelayedPayoutTxSignature(checkNotNull(response.getDelayedPayoutTxBuyerSignature()));
processModel.getTradeWalletService().sellerAddsBuyerWitnessesToDepositTx(
processModel.getDepositTx(),
processModel.getBtcWalletService().getTxFromSerializedTx(response.getDepositTx())
);
// update to the latest peer address of our peer if the message is correct
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -54,6 +54,8 @@ public class SellerPublishesDepositTx extends TradeTask {
processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(processModel.getOffer().getId(),
AddressEntry.Context.RESERVED_FOR_TRADE);
processModel.getTradeManager().requestPersistence();
complete();
} else {
log.warn("We got the onSuccess callback called after the timeout has been triggered a complete().");

View file

@ -47,10 +47,13 @@ public class SellerSendDelayedPayoutTxSignatureRequest extends TradeTask {
Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx(),
"processModel.getPreparedDelayedPayoutTx() must not be null");
byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature(),
"processModel.getDelayedPayoutTxSignature() must not be null");
DelayedPayoutTxSignatureRequest message = new DelayedPayoutTxSignatureRequest(UUID.randomUUID().toString(),
processModel.getOfferId(),
processModel.getMyNodeAddress(),
preparedDelayedPayoutTx.bitcoinSerialize());
preparedDelayedPayoutTx.bitcoinSerialize(),
delayedPayoutTxSignature);
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();
log.info("Send {} to peer {}. tradeId={}, uid={}",

View file

@ -65,6 +65,7 @@ public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG);
log.info("Sent PayoutTxPublishedMessage: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
@ -72,6 +73,7 @@ public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG);
log.info("PayoutTxPublishedMessage arrived: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
@ -79,6 +81,7 @@ public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG);
log.info("PayoutTxPublishedMessage storedInMailbox: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
@ -86,6 +89,7 @@ public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG);
log.error("PayoutTxPublishedMessage failed: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override

View file

@ -76,11 +76,15 @@ public class SellerSendsDepositTxAndDelayedPayoutTxMessage extends SendMailboxMe
@Override
protected void setStateSent() {
trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setStateIfValidTransitionTo(Trade.State.SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG);
processModel.getTradeManager().requestPersistence();
cleanup();
// Complete is called in base class
}
@ -94,6 +98,8 @@ public class SellerSendsDepositTxAndDelayedPayoutTxMessage extends SendMailboxMe
@Override
protected void setStateStoredInMailbox() {
trade.setStateIfValidTransitionTo(Trade.State.SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG);
processModel.getTradeManager().requestPersistence();
// The DepositTxAndDelayedPayoutTxMessage is a mailbox message as earlier we use only the deposit tx which can
// be also received from the network once published.
// Now we send the delayed payout tx as well and with that this message is mandatory for continuing the protocol.
@ -119,6 +125,8 @@ public class SellerSendsDepositTxAndDelayedPayoutTxMessage extends SendMailboxMe
if (!trade.isDepositConfirmed()) {
tryToSendAgainLater();
}
processModel.getTradeManager().requestPersistence();
}
@Override
@ -173,6 +181,8 @@ public class SellerSendsDepositTxAndDelayedPayoutTxMessage extends SendMailboxMe
if (newValue == MessageState.ACKNOWLEDGED) {
// We treat a ACK like SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG
trade.setStateIfValidTransitionTo(Trade.State.SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG);
processModel.getTradeManager().requestPersistence();
cleanup();
complete();
}

View file

@ -100,6 +100,8 @@ public class SellerSignAndFinalizePayoutTx extends TradeTask {
trade.setPayoutTx(transaction);
processModel.getTradeManager().requestPersistence();
walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.MULTI_SIG);
complete();

View file

@ -24,6 +24,7 @@ import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.crypto.DeterministicKey;
@ -47,6 +48,9 @@ public class SellerSignsDelayedPayoutTx extends TradeTask {
Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
BtcWalletService btcWalletService = processModel.getBtcWalletService();
NetworkParameters params = btcWalletService.getParams();
Transaction preparedDepositTx = new Transaction(params, processModel.getPreparedDepositTx());
String id = processModel.getOffer().getId();
byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey();
@ -59,12 +63,15 @@ public class SellerSignsDelayedPayoutTx extends TradeTask {
byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx(
preparedDelayedPayoutTx,
preparedDepositTx,
myMultiSigKeyPair,
buyerMultiSigPubKey,
sellerMultiSigPubKey);
processModel.setDelayedPayoutTxSignature(delayedPayoutTxSignature);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -78,6 +78,8 @@ public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask {
.add(offer.getBuyerSecurityDeposit());
List<RawTransactionInput> takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs());
checkArgument(takerRawTransactionInputs.stream().allMatch(processModel.getTradeWalletService()::isP2WH),
"all takerRawTransactionInputs must be P2WH");
long takerChangeOutputValue = tradingPeer.getChangeOutputValue();
String takerChangeAddressString = tradingPeer.getChangeOutputAddress();
Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress();
@ -103,6 +105,8 @@ public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask {
processModel.setPreparedDepositTx(result.depositTransaction);
processModel.setRawTransactionInputs(result.rawMakerInputs);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -48,6 +48,8 @@ public class SellerAsMakerFinalizesDepositTx extends TradeTask {
processModel.setDepositTx(myDepositTx);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -43,7 +43,7 @@ public class SellerAsMakerProcessDepositTxMessage extends TradeTask {
Validator.checkTradeId(processModel.getOfferId(), message);
checkNotNull(message);
processModel.getTradingPeer().setPreparedDepositTx(checkNotNull(message.getDepositTx()));
processModel.getTradingPeer().setPreparedDepositTx(checkNotNull(message.getDepositTxWithoutWitnesses()));
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
// When we receive that message the taker has published the taker fee, so we apply it to the trade.
@ -51,6 +51,8 @@ public class SellerAsMakerProcessDepositTxMessage extends TradeTask {
// but that cannot be changed due backward compatibility issues. It is a left over from the old trade protocol.
trade.setTakerFeeTxId(processModel.getTakeOfferFeeTxId());
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -41,6 +41,10 @@ public class SellerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInpu
// before we have received his signature for the delayed payout tx.
input.setScriptSig(new Script(new byte[]{}));
});
return preparedDepositTx.bitcoinSerialize();
processModel.getTradeManager().requestPersistence();
// Make sure witnesses are removed as well before sending, to cover the segwit case.
return preparedDepositTx.bitcoinSerialize(false);
}
}

View file

@ -57,6 +57,8 @@ public class SellerAsTakerCreatesDepositTxInputs extends TradeTask {
processModel.setChangeOutputValue(result.changeOutputValue);
processModel.setChangeOutputAddress(result.changeOutputAddress);
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

Some files were not shown because too many files have changed in this diff Show more